chiark / gitweb /
Add new `knock' protocol.
[tripe] / mon / tripemon.in
1 #! @PYTHON@
2 ### -*- mode: python; coding: utf-8 -*-
3 ###
4 ### Graphical monitor for tripe server
5 ###
6 ### (c) 2007 Straylight/Edgeware
7 ###
8
9 ###----- Licensing notice ---------------------------------------------------
10 ###
11 ### This file is part of Trivial IP Encryption (TrIPE).
12 ###
13 ### TrIPE is free software: you can redistribute it and/or modify it under
14 ### the terms of the GNU General Public License as published by the Free
15 ### Software Foundation; either version 3 of the License, or (at your
16 ### option) any later version.
17 ###
18 ### TrIPE is distributed in the hope that it will be useful, but WITHOUT
19 ### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
20 ### FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
21 ### for more details.
22 ###
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
25
26 ###--------------------------------------------------------------------------
27 ### Dependencies.
28
29 import socket as S
30 import tripe as T
31 import mLib as M
32 from sys import argv, exit, stdin, stdout, stderr, exc_info, excepthook
33 import os as OS
34 from os import environ
35 import math as MATH
36 import sets as SET
37 from optparse import OptionParser
38 import time as TIME
39 import re as RX
40 from cStringIO import StringIO
41
42 try:
43   if OS.getenv('TRIPEMON_FORCE_GI'): raise ImportError()
44   import pygtk
45   pygtk.require('2.0')
46   import gtk as G
47   import gobject as GO
48   import gtk.gdk as GDK
49   GL = GO
50   GDK.KEY_Escape = G.keysyms.Escape
51   def raise_window(w): w.window.raise_()
52   combo_box_text = G.combo_box_new_text
53   def set_entry_bg(e, c): e.modify_base(G.STATE_NORMAL, c)
54 except ImportError:
55   from gi.repository import GObject as GO, GLib as GL, Gtk as G, Gdk as GDK
56   G.WINDOW_TOPLEVEL = G.WindowType.TOPLEVEL
57   G.EXPAND = G.AttachOptions.EXPAND
58   G.SHRINK = G.AttachOptions.SHRINK
59   G.FILL = G.AttachOptions.FILL
60   G.SORT_ASCENDING = G.SortType.ASCENDING
61   G.POLICY_AUTOMATIC = G.PolicyType.AUTOMATIC
62   G.SHADOW_IN = G.ShadowType.IN
63   G.SELECTION_NONE = G.SelectionMode.NONE
64   G.DIALOG_MODAL = G.DialogFlags.MODAL
65   G.RESPONSE_CANCEL = G.ResponseType.CANCEL
66   G.RESPONSE_NONE = G.ResponseType.NONE
67   def raise_window(w): getattr(w.get_window(), 'raise')()
68   combo_box_text = G.ComboBoxText
69   def set_entry_bg(e, c): e.modify_bg(G.StateType.NORMAL, c)
70
71 if OS.getenv('TRIPE_DEBUG_MONITOR') is not None:
72   T._debug = 1
73
74 ###--------------------------------------------------------------------------
75 ### Doing things later.
76
77 def uncaught():
78   """Report an uncaught exception."""
79   excepthook(*exc_info())
80
81 def xwrap(func):
82   """
83   Return a function which behaves like FUNC, but reports exceptions via
84   uncaught.
85   """
86   def _(*args, **kw):
87     try:
88       return func(*args, **kw)
89     except SystemExit:
90       raise
91     except:
92       uncaught()
93       raise
94   return _
95
96 def invoker(func, *args, **kw):
97   """
98   Return a function which throws away its arguments and calls
99   FUNC(*ARGS, **KW).
100
101   If for loops worked by binding rather than assignment then we wouldn't need
102   this kludge.
103   """
104   return lambda *hunoz, **hukairz: xwrap(func)(*args, **kw)
105
106 def cr(func, *args, **kw):
107   """Return a function which invokes FUNC(*ARGS, **KW) in a coroutine."""
108   name = T.funargstr(func, args, kw)
109   return lambda *hunoz, **hukairz: \
110          T.Coroutine(xwrap(func), name = name).switch(*args, **kw)
111
112 def incr(func):
113   """Decorator: runs its function in a coroutine of its own."""
114   return lambda *args, **kw: \
115          (T.Coroutine(func, name = T.funargstr(func, args, kw))
116           .switch(*args, **kw))
117
118 ###--------------------------------------------------------------------------
119 ### Random bits of infrastructure.
120
121 ## Program name, shorn of extraneous stuff.
122 M.ego(argv[0])
123 moan = M.moan
124 die = M.die
125
126 class HookList (object):
127   """
128   Notification hook list.
129
130   Other objects can add functions onto the hook list.  When the hook list is
131   run, the functions are called in the order in which they were registered.
132   """
133
134   def __init__(me):
135     """Basic initialization: create the hook list."""
136     me.list = []
137
138   def add(me, func, obj):
139     """Add FUNC to the list of hook functions."""
140     me.list.append((obj, func))
141
142   def prune(me, obj):
143     """Remove hook functions registered with the given OBJ."""
144     new = []
145     for o, f in me.list:
146       if o is not obj:
147         new.append((o, f))
148     me.list = new
149
150   def run(me, *args, **kw):
151     """Invoke the hook functions with arguments *ARGS and **KW."""
152     for o, hook in me.list:
153       rc = hook(*args, **kw)
154       if rc is not None: return rc
155     return None
156
157 class HookClient (object):
158   """
159   Mixin for classes which are clients of hooks.
160
161   It keeps track of the hooks it's a client of, and has the ability to
162   extricate itself from all of them.  This is useful because weak objects
163   don't seem to work well.
164   """
165   def __init__(me):
166     """Basic initialization."""
167     me.hooks = SET.Set()
168
169   def hook(me, hk, func):
170     """Add FUNC to the hook list HK."""
171     hk.add(func, me)
172     me.hooks.add(hk)
173
174   def unhook(me, hk):
175     """Remove myself from the hook list HK."""
176     hk.prune(me)
177     me.hooks.discard(hk)
178
179   def unhookall(me):
180     """Remove myself from all hook lists."""
181     for hk in me.hooks:
182       hk.prune(me)
183     me.hooks.clear()
184
185 class struct (object):
186   """A very simple dumb data container object."""
187   def __init__(me, **kw):
188     me.__dict__.update(kw)
189
190 ## Matches ISO date format yyyy-mm-ddThh:mm:ss.
191 rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
192
193 ###--------------------------------------------------------------------------
194 ### Connections.
195
196 class GIOWatcher (object):
197   """
198   Monitor I/O events using glib.
199   """
200   def __init__(me, conn, mc = GL.main_context_default()):
201     me._conn = conn
202     me._watch = None
203     me._mc = mc
204   def connected(me, sock):
205     me._watch = GL.io_add_watch(sock, GL.IO_IN,
206                                 lambda *hunoz: me._conn.receive())
207   def disconnected(me):
208     GL.source_remove(me._watch)
209     me._watch = None
210   def iterate(me):
211     me._mc.iteration(True)
212
213 class Connection (T.TripeCommandDispatcher):
214   """
215   The main connection to the server.
216
217   The improvement over the TripeCommandDispatcher is that the Connection
218   provides hooklists for NOTE, WARN and TRACE messages, and for connect and
219   disconnect events.
220
221   This class knows about the Glib I/O dispatcher system, and plugs into it.
222
223   Hooks:
224
225     * connecthook(): a connection to the server has been established
226     * disconnecthook(): the connection has been dropped
227     * notehook(TOKEN, ...): server issued a notification
228     * warnhook(TOKEN, ...): server issued a warning
229     * tracehook(TOKEN, ...): server issued a trace message
230   """
231
232   def __init__(me, socket):
233     """Create a new Connection."""
234     T.TripeCommandDispatcher.__init__(me, socket)
235     me.connecthook = HookList()
236     me.disconnecthook = HookList()
237     me.notehook = HookList()
238     me.warnhook = HookList()
239     me.tracehook = HookList()
240     me.handler['NOTE'] = lambda _, *rest: me.notehook.run(*rest)
241     me.handler['WARN'] = lambda _, *rest: me.warnhook.run(*rest)
242     me.handler['TRACE'] = lambda _, *rest: me.tracehook.run(*rest)
243     me.iowatch = GIOWatcher(me)
244
245   def connected(me):
246     """Handles reconnection to the server, and signals the hook."""
247     T.TripeCommandDispatcher.connected(me)
248     me.connecthook.run()
249
250   def disconnected(me, reason):
251     """Handles disconnection from the server, and signals the hook."""
252     me.disconnecthook.run(reason)
253     T.TripeCommandDispatcher.disconnected(me, reason)
254
255 ###--------------------------------------------------------------------------
256 ### Watching the peers go by.
257
258 class MonitorObject (object):
259   """
260   An object with hooks it uses to notify others of changes in its state.
261   These are the objects tracked by the MonitorList class.
262
263   The object has a name, an `aliveness' state indicated by the `alivep' flag,
264   and hooks.
265
266   Hooks:
267
268     * changehook(): the object has changed its state
269     * deadhook(): the object has been destroyed
270
271   Subclass responsibilities:
272
273     * update(INFO): update internal state based on the provided INFO, and run
274       the changehook.
275   """
276
277   def __init__(me, name):
278     """Initialize the object with the given NAME."""
279     me.name = name
280     me.deadhook = HookList()
281     me.changehook = HookList()
282     me.alivep = True
283
284   def dead(me):
285     """Mark the object as dead; invoke the deadhook."""
286     me.alivep = False
287     me.deadhook.run()
288
289 class Peer (MonitorObject):
290   """
291   An object representing a connected peer.
292
293   As well as the standard hooks, a peer has a pinghook, which isn't used
294   directly by this class.
295
296   Hooks:
297
298     * pinghook(): invoked by the Pinger (q.v.) when ping statistics change
299
300   Attributes provided are:
301
302     * addr = a vaguely human-readable representation of the peer's address
303     * ifname = the peer's interface name
304     * tunnel = the kind of tunnel the peer is using
305     * keepalive = the peer's keepalive interval in seconds
306     * ping['EPING'] and ping['PING'] = pingstate statistics (maintained by
307       the Pinger)
308   """
309
310   def __init__(me, name):
311     """Initialize the object with the given name."""
312     MonitorObject.__init__(me, name)
313     me.pinghook = HookList()
314     me.__dict__.update(conn.algs(name))
315     me.update()
316
317   def update(me, hunoz = None):
318     """Update the peer, fetching information about it from the server."""
319     me._setaddr(conn.addr(me.name))
320     me.ifname = conn.ifname(me.name)
321     me.__dict__.update(conn.peerinfo(me.name))
322     me.changehook.run()
323
324   def _setaddr(me, addr):
325     """Set the peer's address."""
326     if addr[0] == 'INET':
327       ipaddr, port = addr[1:]
328       try:
329         name = S.gethostbyaddr(ipaddr)[0]
330         me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
331       except S.herror:
332         me.addr = 'INET %s:%s' % (ipaddr, port)
333     else:
334       me.addr = ' '.join(addr)
335
336   def setaddr(me, addr):
337     """Informs the object of a change to its address to ADDR."""
338     me._setaddr(addr)
339     me.changehook.run()
340
341   def setifname(me, newname):
342     """Informs the object of a change to its interface name to NEWNAME."""
343     me.ifname = newname
344     me.changehook.run()
345
346 class Service (MonitorObject):
347   """
348   Represents a service.
349
350   Additional attributes are:
351
352     * version = the service version
353   """
354   def __init__(me, name, version):
355     MonitorObject.__init__(me, name)
356     me.version = version
357
358   def update(me, version):
359     """Tell the Service that its version has changed to VERSION."""
360     me.version = version
361     me.changehook.run()
362
363 class MonitorList (object):
364   """
365   Maintains a collection of MonitorObjects.
366
367   The MonitorList can be indexed by name to retrieve the individual objects;
368   iteration generates the individual objects.  More complicated operations
369   can be done on the `table' dictionary directly.
370
371   Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
372   deleted.
373
374   Subclass responsibilities:
375
376     * list(): return a list of (NAME, INFO) pairs.
377
378     * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
379       is from the output of list().
380   """
381
382   def __init__(me):
383     """Initialize a new MonitorList."""
384     me.table = {}
385     me.addhook = HookList()
386     me.delhook = HookList()
387
388   def update(me):
389     """
390     Refresh the list of objects:
391
392     We add new object which have appeared, delete ones which have vanished,
393     and update any which persist.
394     """
395     new = {}
396     for name, stuff in me.list():
397       new[name] = True
398       me.add(name, stuff)
399     for name in me.table.copy():
400       if name not in new:
401         me.remove(name)
402
403   def add(me, name, stuff):
404     """
405     Add a new object created by make(NAME, STUFF) if it doesn't already
406     exist.  If it does, update it.
407     """
408     if name not in me.table:
409       obj = me.make(name, stuff)
410       me.table[name] = obj
411       me.addhook.run(obj)
412     else:
413       me.table[name].update(stuff)
414
415   def remove(me, name):
416     """
417     Remove the object called NAME from the list.
418
419     The object becomes dead.
420     """
421     if name in me.table:
422       obj = me.table[name]
423       del me.table[name]
424       me.delhook.run(obj)
425       obj.dead()
426
427   def __getitem__(me, name):
428     """Retrieve the object called NAME."""
429     return me.table[name]
430
431   def __iter__(me):
432     """Iterate over the objects."""
433     return me.table.itervalues()
434
435 class PeerList (MonitorList):
436   """The list of the known peers."""
437   def list(me):
438     return [(name, None) for name in conn.list()]
439   def make(me, name, stuff):
440     return Peer(name)
441
442 class ServiceList (MonitorList):
443   """The list of the registered services."""
444   def list(me):
445     return conn.svclist()
446   def make(me, name, stuff):
447     return Service(name, stuff)
448
449 class Monitor (HookClient):
450   """
451   The main monitor: keeps track of the changes happening to the server.
452
453   Exports the peers, services MonitorLists, and a (plain Python) list
454   autopeers of peers which the connect service knows how to start by name.
455
456   Hooks provided:
457
458     * autopeershook(): invoked when the auto-peers list changes.
459   """
460   def __init__(me):
461     """Initialize the Monitor."""
462     HookClient.__init__(me)
463     me.peers = PeerList()
464     me.services = ServiceList()
465     me.hook(conn.connecthook, me._connected)
466     me.hook(conn.notehook, me._notify)
467     me.autopeershook = HookList()
468     me.autopeers = None
469
470   def _connected(me):
471     """Handle a successful connection by starting the setup coroutine."""
472     me._setup()
473
474   @incr
475   def _setup(me):
476     """Coroutine function: initialize for a new connection."""
477     conn.watch('-A+wnt')
478     me.peers.update()
479     me.services.update()
480     me._updateautopeers()
481
482   def _updateautopeers(me):
483     """Update the auto-peers list from the connect service."""
484     if 'connect' in me.services.table:
485       me.autopeers = [' '.join(line)
486                       for line in conn.svcsubmit('connect', 'list-active')]
487       me.autopeers.sort()
488     else:
489       me.autopeers = None
490     me.autopeershook.run()
491
492   def _notify(me, code, *rest):
493     """
494     Handle notifications from the server.
495
496     ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
497     SVCCLAIM and SVCRELEASE get passed up to the ServiceList.  Finally,
498     peerdb-update notifications from the watch service cause us to refresh
499     the auto-peers list.
500     """
501     if code == 'ADD':
502       T.aside(me.peers.add, rest[0], None)
503     elif code == 'KILL':
504       T.aside(me.peers.remove, rest[0])
505     elif code == 'NEWIFNAME':
506       try:
507         me.peers[rest[0]].setifname(rest[2])
508       except KeyError:
509         pass
510     elif code == 'NEWADDR':
511       try:
512         me.peers[rest[0]].setaddr(rest[1:])
513       except KeyError:
514         pass
515     elif code == 'SVCCLAIM':
516       T.aside(me.services.add, rest[0], rest[1])
517       if rest[0] == 'connect':
518         T.aside(me._updateautopeers)
519     elif code == 'SVCRELEASE':
520       T.aside(me.services.remove, rest[0])
521       if rest[0] == 'connect':
522         T.aside(me._updateautopeers)
523     elif code == 'USER':
524       if not rest: return
525       if rest[0] == 'watch' and \
526          rest[1] == 'peerdb-update':
527         T.aside(me._updateautopeers)
528
529 ###--------------------------------------------------------------------------
530 ### Window management cruft.
531
532 class MyWindowMixin (G.Window, HookClient):
533   """
534   Mixin for windows which call a closehook when they're destroyed.  It's also
535   a hookclient, and will release its hooks when it's destroyed.
536
537   Hooks:
538
539     * closehook(): called when the window is closed.
540   """
541
542   def mywininit(me):
543     """Initialization function.  Note that it's not called __init__!"""
544     me.closehook = HookList()
545     HookClient.__init__(me)
546     me.connect('destroy', invoker(me.close))
547
548   def close(me):
549     """Close the window, invoking the closehook and releasing all hooks."""
550     me.closehook.run()
551     me.destroy()
552     me.unhookall()
553
554 class MyWindow (MyWindowMixin):
555   """A version of MyWindowMixin suitable as a single parent class."""
556   def __init__(me, kind = G.WINDOW_TOPLEVEL):
557     G.Window.__init__(me, kind)
558     me.mywininit()
559
560 class TrivialWindowMixin (MyWindowMixin):
561   """A simple window which you can close with Escape."""
562   def mywininit(me):
563     super(TrivialWindowMixin, me).mywininit()
564     me.connect('key-press-event', me._keypress)
565   def _keypress(me, _, ev):
566     if ev.keyval == GDK.KEY_Escape: me.destroy()
567
568 class TrivialWindow (MyWindow, TrivialWindowMixin):
569   pass
570
571 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
572   """A dialogue box with a closehook and sensible button binding."""
573
574   def __init__(me, title = None, flags = 0, buttons = []):
575     """
576     The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
577     THUNK when the button is pressed.  The other arguments are just like
578     GTK's Dialog class.
579     """
580     i = 0
581     br = []
582     me.rmap = []
583     for b, f in buttons:
584       br.append(b)
585       br.append(i)
586       me.rmap.append(f)
587       i += 1
588     G.Dialog.__init__(me, title, None, flags, tuple(br))
589     me.mywininit()
590     me.set_default_response(i - 1)
591     me.connect('response', me.respond)
592
593   def respond(me, hunoz, rid, *hukairz):
594     """Dispatch responses to the appropriate thunks."""
595     if rid >= 0: me.rmap[rid]()
596
597 def makeactiongroup(name, acts):
598   """
599   Creates an ActionGroup called NAME.
600
601   ACTS is a list of tuples containing:
602
603     * ACT: an action name
604     * LABEL: the label string for the action
605     * ACCEL: accelerator string, or None
606     * FUNC: thunk to call when the action is invoked
607   """
608   actgroup = G.ActionGroup(name)
609   for act, label, accel, func in acts:
610     a = G.Action(act, label, None, None)
611     if func: a.connect('activate', invoker(func))
612     actgroup.add_action_with_accel(a, accel)
613   return actgroup
614
615 class GridPacker (G.Table):
616   """
617   Like a Table, but with more state: makes filling in the widgets easier.
618   """
619
620   def __init__(me):
621     """Initialize a new GridPacker."""
622     G.Table.__init__(me)
623     me.row = 0
624     me.col = 0
625     me.rows = 1
626     me.cols = 1
627     me.set_border_width(4)
628     me.set_col_spacings(4)
629     me.set_row_spacings(4)
630
631   def pack(me, w, width = 1, newlinep = False,
632            xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
633            xpad = 0, ypad = 0):
634     """
635     Packs a new widget.
636
637     W is the widget to add.  XOPY, YOPT, XPAD and YPAD are as for Table.
638     WIDTH is how many cells to take up horizontally.  NEWLINEP is whether to
639     start a new line for this widget.  Returns W.
640     """
641     if newlinep:
642       me.row += 1
643       me.col = 0
644     bot = me.row + 1
645     right = me.col + width
646     if bot > me.rows or right > me.cols:
647       if bot > me.rows: me.rows = bot
648       if right > me.cols: me.cols = right
649       me.resize(me.rows, me.cols)
650     me.attach(w, me.col, me.col + width, me.row, me.row + 1,
651               xopt, yopt, xpad, ypad)
652     me.col += width
653     return w
654
655   def labelled(me, lab, w, newlinep = False, **kw):
656     """
657     Packs a labelled widget.
658
659     Other arguments are as for pack.  Returns W.
660     """
661     label = G.Label(lab + ' ')
662     label.set_alignment(1.0, 0)
663     me.pack(label, newlinep = newlinep, xopt = G.FILL)
664     me.pack(w, **kw)
665     return w
666
667   def info(me, label, text = None, len = 18, **kw):
668     """
669     Packs an information widget with a label.
670
671     LABEL is the label; TEXT is the initial text; LEN is the estimated length
672     in characters.  Returns the entry widget.
673     """
674     e = G.Label()
675     if text is not None: e.set_text(text)
676     e.set_width_chars(len)
677     e.set_selectable(True)
678     e.set_alignment(0.0, 0.5)
679     me.labelled(label, e, **kw)
680     return e
681
682 class WindowSlot (HookClient):
683   """
684   A place to store a window -- specificially a MyWindowMixin.
685
686   If the window is destroyed, remember this; when we come to open the window,
687   raise it if it already exists; otherwise make a new one.
688   """
689   def __init__(me, createfunc):
690     """
691     Constructor: CREATEFUNC must return a new Window which supports the
692     closehook protocol.
693     """
694     HookClient.__init__(me)
695     me.createfunc = createfunc
696     me.window = None
697
698   def open(me):
699     """Opens the window, creating it if necessary."""
700     if me.window:
701       raise_window(me.window)
702     else:
703       me.window = me.createfunc()
704       me.hook(me.window.closehook, me.closed)
705
706   def closed(me):
707     """Handles the window being closed."""
708     me.unhook(me.window.closehook)
709     me.window = None
710
711 class MyTreeView (G.TreeView):
712   def __init__(me, model):
713     G.TreeView.__init__(me, model)
714     me.set_rules_hint(True)
715
716 class MyScrolledWindow (G.ScrolledWindow):
717   def __init__(me):
718     G.ScrolledWindow.__init__(me)
719     me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
720     me.set_shadow_type(G.SHADOW_IN)
721
722 ## Matches a signed integer.
723 rx_num = RX.compile(r'^[-+]?\d+$')
724
725 ## The colour red.
726 c_red = GDK.color_parse('#ff6666')
727
728 class ValidationError (Exception):
729   """Raised by ValidatingEntry.get_text() if the text isn't valid."""
730   pass
731
732 class ValidatingEntry (G.Entry):
733   """
734   Like an Entry, but makes the text go red if the contents are invalid.
735
736   If get_text is called, and the text is invalid, ValidationError is raised.
737   The attribute validp reflects whether the contents are currently valid.
738   """
739
740   def __init__(me, valid, text = '', size = -1, *arg, **kw):
741     """
742     Make a validating Entry.
743
744     VALID is a regular expression or a predicate on strings.  TEXT is the
745     default text to insert.  SIZE is the size of the box to set, in
746     characters (ish).  Other arguments are passed to Entry.
747     """
748     G.Entry.__init__(me, *arg, **kw)
749     me.connect("changed", me._check)
750     me.connect("state-changed", me._check)
751     if callable(valid):
752       me.validate = valid
753     else:
754       me.validate = RX.compile(valid).match
755     me.ensure_style()
756     if size != -1: me.set_width_chars(size)
757     me.set_activates_default(True)
758     me.set_text(text)
759     me._check()
760
761   def _check(me, *hunoz):
762     """Check the current text and update validp and the text colour."""
763     if me.validate(G.Entry.get_text(me)):
764       me.validp = True
765       set_entry_bg(me, None)
766     else:
767       me.validp = False
768       set_entry_bg(me, me.is_sensitive() and c_red or None)
769
770   def get_text(me):
771     """
772     Return the text in the Entry if it's valid.  If it isn't, raise
773     ValidationError.
774     """
775     if not me.validp:
776       raise ValidationError()
777     return G.Entry.get_text(me)
778
779 def numericvalidate(min = None, max = None):
780   """
781   Return a validation function for numbers.
782
783   Entry must consist of an optional sign followed by digits, and the
784   resulting integer must be within the given bounds.
785   """
786   return lambda x: (rx_num.match(x) and
787                     (min is None or long(x) >= min) and
788                     (max is None or long(x) <= max))
789
790 ###--------------------------------------------------------------------------
791 ### Various minor dialog boxen.
792
793 GPL = """\
794 TrIPE is free software: you can redistribute it and/or modify it under
795 the terms of the GNU General Public License as published by the Free
796 Software Foundation; either version 3 of the License, or (at your
797 option) any later version.
798
799 TrIPE is distributed in the hope that it will be useful, but WITHOUT
800 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
801 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
802 for more details.
803
804 You should have received a copy of the GNU General Public License
805 along with TrIPE.  If not, see <https://www.gnu.org/licenses/>."""
806
807 class AboutBox (G.AboutDialog, TrivialWindowMixin):
808   """The program `About' box."""
809   def __init__(me):
810     G.AboutDialog.__init__(me)
811     me.mywininit()
812     me.set_name('TrIPEmon')
813     me.set_version(T.VERSION)
814     me.set_license(GPL)
815     me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
816     me.set_comments('A graphical monitor for the TrIPE VPN server')
817     me.set_copyright('Copyright Â© 2006-2008 Straylight/Edgeware')
818     me.connect('response', me.respond)
819     me.show()
820   def respond(me, hunoz, rid, *hukairz):
821     if rid == G.RESPONSE_CANCEL:
822       me.close()
823 aboutbox = WindowSlot(AboutBox)
824
825 def moanbox(msg):
826   """Report an error message in a window."""
827   d = G.Dialog('Error from %s' % M.quis,
828                flags = G.DIALOG_MODAL,
829                buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
830   label = G.Label(msg)
831   label.set_padding(20, 20)
832   d.vbox.pack_start(label, True, True, 0)
833   label.show()
834   d.run()
835   d.destroy()
836
837 def unimplemented(*hunoz):
838   """Indicator of laziness."""
839   moanbox("I've not written that bit yet.")
840
841 ###--------------------------------------------------------------------------
842 ### Logging windows.
843
844 class LogModel (G.ListStore):
845   """
846   A simple list of log messages, usable as the model for a TreeView.
847
848   The column headings are stored in the `cols' attribute.
849   """
850
851   def __init__(me, columns):
852     """
853     COLUMNS must be a list of column name strings.  We add a time column to
854     the left.
855     """
856     me.cols = ('Time',) + columns
857     G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
858
859   def add(me, *entries):
860     """
861     Adds a new log message, with a timestamp.
862
863     The ENTRIES are the contents for the list columns.
864     """
865     now = TIME.strftime('%Y-%m-%d %H:%M:%S')
866     me.append((now, ) + entries)
867
868 class TraceLogModel (LogModel):
869   """Log model for trace messages."""
870   def __init__(me):
871     LogModel.__init__(me, ('Message',))
872   def notify(me, line):
873     """Call with a new trace message."""
874     me.add(line)
875
876 class WarningLogModel (LogModel):
877   """
878   Log model for warnings.
879
880   We split the category out into a separate column.
881   """
882   def __init__(me):
883     LogModel.__init__(me, ('Category', 'Message'))
884   def notify(me, tag, *rest):
885     """Call with a new warning message."""
886     me.add(tag, ' '.join([T.quotify(w) for w in rest]))
887
888 class LogViewer (TrivialWindow):
889   """
890   A log viewer window.
891
892   Its contents are a TreeView showing the log.
893
894   Attributes:
895
896     * model: an appropriate LogModel
897     * list: a TreeView widget to display the log
898   """
899
900   def __init__(me, model):
901     """
902     Create a log viewer showing the LogModel MODEL.
903     """
904     TrivialWindow.__init__(me)
905     me.model = model
906     scr = MyScrolledWindow()
907     me.list = MyTreeView(me.model)
908     i = 0
909     for c in me.model.cols:
910       crt = G.CellRendererText()
911       me.list.append_column(G.TreeViewColumn(c, crt, text = i))
912       i += 1
913     crt.set_property('family', 'monospace')
914     me.set_default_size(440, 256)
915     scr.add(me.list)
916     me.add(scr)
917     me.show_all()
918
919 ###--------------------------------------------------------------------------
920 ### Pinging peers.
921
922 class pingstate (struct):
923   """
924   Information kept for each peer by the Pinger.
925
926   Important attributes:
927
928     * peer = the peer name
929     * command = PING or EPING
930     * n = how many pings we've sent so far
931     * ngood = how many returned
932     * nmiss = how many didn't return
933     * nmissrun = how many pings since the last good one
934     * tlast = round-trip time for the last (good) ping
935     * ttot = total roung trip time
936   """
937   pass
938
939 class Pinger (T.Coroutine, HookClient):
940   """
941   Coroutine which pings known peers and collects statistics.
942
943   Interesting attributes:
944
945     * _map: dict mapping peer names to Peer objects
946     * _q: event queue for notifying pinger coroutine
947     * _timer: gobject timer for waking the coroutine
948   """
949
950   def __init__(me):
951     """
952     Initialize the pinger.
953
954     We watch the monitor's PeerList to track which peers we should ping.  We
955     maintain an event queue and put all the events on that.
956
957     The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
958     where CMD is 'PING' or 'EPING'.
959     """
960     T.Coroutine.__init__(me)
961     HookClient.__init__(me)
962     me._map = {}
963     me._q = T.Queue()
964     me._timer = None
965     me.hook(conn.connecthook, me._connected)
966     me.hook(conn.disconnecthook, me._disconnected)
967     me.hook(monitor.peers.addhook,
968             lambda p: T.defer(me._q.put, (p, 'ADD', None)))
969     me.hook(monitor.peers.delhook,
970             lambda p: T.defer(me._q.put, (p, 'KILL', None)))
971     if conn.connectedp(): me.connected()
972
973   def _connected(me):
974     """Respond to connection: start pinging thngs."""
975     me._timer = GL.timeout_add(1000, me._timerfunc)
976
977   def _timerfunc(me):
978     """Timer function: put a timer event on the queue."""
979     me._q.put((None, 'TIMER', None))
980     return True
981
982   def _disconnected(me, reason):
983     """Respond to disconnection: stop pinging."""
984     GL.source_remove(me._timer)
985
986   def run(me):
987     """
988     Coroutine function: read events from the queue and process them.
989
990     Interesting events:
991
992       * (PEER, 'KILL', None): remove PEER from the interesting peers list
993       * (PEER, 'ADD', None): add PEER to the list
994       * (PEER, 'INFO', TOKENS): result from a PING command
995       * (None, 'TIMER', None): interval timer went off: send more pings
996     """
997     while True:
998       tag, code, stuff = me._q.get()
999       if code == 'KILL':
1000         name = tag.name
1001         if name in me._map:
1002           del me._map[name]
1003       elif not conn.connectedp():
1004         pass
1005       elif code == 'ADD':
1006         p = tag
1007         p.ping = {}
1008         for cmd in 'PING', 'EPING':
1009           ps = pingstate(command = cmd, peer = p,
1010                          n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1011                          tlast = 0, ttot = 0)
1012           p.ping[cmd] = ps
1013         me._map[p.name] = p
1014       elif code == 'INFO':
1015         ps = tag
1016         if stuff[0] == 'ping-ok':
1017           t = float(stuff[1])
1018           ps.ngood += 1
1019           ps.nmissrun = 0
1020           ps.tlast = t
1021           ps.ttot += t
1022         else:
1023           ps.nmiss += 1
1024           ps.nmissrun += 1
1025         ps.n += 1
1026         ps.peer.pinghook.run(ps.peer, ps.command, ps)
1027       elif code == 'TIMER':
1028         for name, p in me._map.iteritems():
1029           for cmd, ps in p.ping.iteritems():
1030             conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
1031               cmd, '-background', conn.bgtag(), '--', name]))
1032
1033 ###--------------------------------------------------------------------------
1034 ### Random dialogue boxes.
1035
1036 class AddPeerDialog (MyDialog):
1037   """
1038   Let the user create a new peer the low-level way.
1039
1040   Interesting attributes:
1041
1042     * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1043   """
1044
1045   def __init__(me):
1046     """Initialize the dialogue."""
1047     MyDialog.__init__(me, 'Add peer',
1048                       buttons = [(G.STOCK_CANCEL, me.destroy),
1049                                  (G.STOCK_OK, me.ok)])
1050     me._setup()
1051
1052   @incr
1053   def _setup(me):
1054     """Coroutine function: background setup for AddPeerDialog."""
1055     table = GridPacker()
1056     me.vbox.pack_start(table, True, True, 0)
1057     me.e_name = table.labelled('Name',
1058                                ValidatingEntry(r'^[^\s:]+$', '', 16),
1059                                width = 3)
1060     me.e_addr = table.labelled('Address',
1061                                ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1062                                newlinep = True)
1063     me.e_port = table.labelled('Port',
1064                                ValidatingEntry(numericvalidate(0, 65535),
1065                                                '4070',
1066                                                5))
1067     me.l_tunnel = table.labelled('Tunnel', combo_box_text(),
1068                                  newlinep = True, width = 3)
1069     me.tuns = ['(Default)'] + conn.tunnels()
1070     for t in me.tuns:
1071       me.l_tunnel.append_text(t)
1072     me.l_tunnel.set_active(0)
1073
1074     def tickybox_sensitivity(tickybox, target):
1075       tickybox.connect('toggled',
1076                        lambda t: target.set_sensitive (t.get_active()))
1077
1078     def optional_entry(label, rx_valid, width):
1079       c = G.CheckButton(label)
1080       table.pack(c, newlinep = True, xopt = G.FILL)
1081       e = ValidatingEntry(rx_valid, '', width)
1082       e.set_sensitive(False)
1083       tickybox_sensitivity(c, e)
1084       table.pack(e, width = 3)
1085       return c, e
1086
1087     me.c_keepalive, me.e_keepalive = \
1088       optional_entry('Keepalives', r'^\d+[hms]?$', 5)
1089
1090     me.c_cork = G.CheckButton('Cork')
1091     table.pack(me.c_cork, newlinep = True, width = 4, xopt = G.FILL)
1092
1093     me.c_mobile = G.CheckButton('Mobile')
1094     table.pack(me.c_mobile, newlinep = True, width = 4, xopt = G.FILL)
1095
1096     me.c_peerkey, me.e_peerkey = \
1097       optional_entry('Peer key tag', r'^[^.:\s]+$', 16)
1098     me.c_privkey, me.e_privkey = \
1099       optional_entry('Private key tag', r'^[^.:\s]+$', 16)
1100
1101     me.c_knock, me.e_knock = \
1102       optional_entry('Knock string', r'^[^:\s]+$', 16)
1103
1104     me.show_all()
1105
1106   def ok(me):
1107     """Handle an OK press: create the peer."""
1108     try:
1109       t = me.l_tunnel.get_active()
1110       me._addpeer(me.e_name.get_text(),
1111                   me.e_addr.get_text(),
1112                   me.e_port.get_text(),
1113                   keepalive = (me.c_keepalive.get_active() and
1114                                me.e_keepalive.get_text() or None),
1115                   tunnel = t and me.tuns[t] or None,
1116                   cork = me.c_cork.get_active() or None,
1117                   mobile = me.c_mobile.get_active() or None,
1118                   key = (me.c_peerkey.get_active() and
1119                          me.e_peerkey.get_text() or None),
1120                   priv = (me.c_privkey.get_active() and
1121                           me.e_privkey.get_text() or None),
1122                   knock = (me.c_knock.get_active() and
1123                            me.e_knock.get_text() or None))
1124     except ValidationError:
1125       GDK.beep()
1126       return
1127
1128   @incr
1129   def _addpeer(me, *args, **kw):
1130     """Coroutine function: actually do the ADD command."""
1131     try:
1132       conn.add(*args, **kw)
1133       me.destroy()
1134     except T.TripeError, exc:
1135       T.defer(moanbox, ' '.join(exc))
1136
1137 class ServInfo (TrivialWindow):
1138   """
1139   Show information about the server and available services.
1140
1141   Interesting attributes:
1142
1143     * e: maps SERVINFO keys to entry widgets
1144     * svcs: Gtk ListStore describing services (columns are name and version)
1145   """
1146
1147   def __init__(me):
1148     TrivialWindow.__init__(me)
1149     me.set_title('TrIPE server info')
1150     table = GridPacker()
1151     me.add(table)
1152     me.e = {}
1153     def add(label, tag, text = None, **kw):
1154       me.e[tag] = table.info(label, text, **kw)
1155     add('Implementation', 'implementation')
1156     add('Version', 'version', newlinep = True)
1157     me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1158     me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1159     scr = MyScrolledWindow()
1160     lb = MyTreeView(me.svcs)
1161     i = 0
1162     for title in 'Service', 'Version':
1163       lb.append_column(G.TreeViewColumn(
1164         title, G.CellRendererText(), text = i))
1165       i += 1
1166     for svc in monitor.services:
1167       me.svcs.append([svc.name, svc.version])
1168     scr.add(lb)
1169     table.pack(scr, width = 2, newlinep = True,
1170                yopt = G.EXPAND | G.FILL | G.SHRINK)
1171     me.update()
1172     me.hook(conn.connecthook, me.update)
1173     me.hook(monitor.services.addhook, me.addsvc)
1174     me.hook(monitor.services.delhook, me.delsvc)
1175     me.show_all()
1176
1177   def addsvc(me, svc):
1178     me.svcs.append([svc.name, svc.version])
1179
1180   def delsvc(me, svc):
1181     for i in xrange(len(me.svcs)):
1182       if me.svcs[i][0] == svc.name:
1183         me.svcs.remove(me.svcs.get_iter(i))
1184         break
1185   @incr
1186   def update(me):
1187     info = conn.servinfo()
1188     for i in me.e:
1189       me.e[i].set_text(info[i])
1190
1191 class TraceOptions (MyDialog):
1192   """Tracing options window."""
1193   def __init__(me):
1194     MyDialog.__init__(me, title = 'Tracing options',
1195                       buttons = [(G.STOCK_CLOSE, me.destroy),
1196                                  (G.STOCK_OK, cr(me.ok))])
1197     me._setup()
1198
1199   @incr
1200   def _setup(me):
1201     me.opts = []
1202     for ch, st, desc in conn.trace():
1203       if ch.isupper(): continue
1204       text = desc[0].upper() + desc[1:]
1205       ticky = G.CheckButton(text)
1206       ticky.set_active(st == '+')
1207       me.vbox.pack_start(ticky, True, True, 0)
1208       me.opts.append((ch, ticky))
1209     me.show_all()
1210   def ok(me):
1211     on = []
1212     off = []
1213     for ch, ticky in me.opts:
1214       if ticky.get_active():
1215         on.append(ch)
1216       else:
1217         off.append(ch)
1218     setting = ''.join(on) + '-' + ''.join(off)
1219     conn.trace(setting)
1220     me.destroy()
1221
1222 ###--------------------------------------------------------------------------
1223 ### Peer window.
1224
1225 def xlate_time(t):
1226   """Translate a TrIPE-format time to something human-readable."""
1227   if t == 'NEVER': return '(never)'
1228   YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1229   ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1230   ago = MATH.floor(ago); unit = 's'
1231   for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1232     if ago < 2*n: break
1233     ago /= n
1234     unit = u
1235   return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1236          (YY, MM, DD, hh, mm, ss, ago, unit)
1237 def xlate_bytes(b):
1238   """Translate a raw byte count into something a human might want to read."""
1239   suff = 'B'
1240   b = int(b)
1241   for s in 'KMG':
1242     if b < 4096: break
1243     b /= 1024
1244     suff = s
1245   return '%d %s' % (b, suff)
1246
1247 ## How to translate peer stats.  Maps the stat name to a translation
1248 ## function.
1249 statsxlate = \
1250   [('start-time', xlate_time),
1251    ('last-packet-time', xlate_time),
1252    ('last-keyexch-time', xlate_time),
1253    ('bytes-in', xlate_bytes),
1254    ('bytes-out', xlate_bytes),
1255    ('keyexch-bytes-in', xlate_bytes),
1256    ('keyexch-bytes-out', xlate_bytes),
1257    ('ip-bytes-in', xlate_bytes),
1258    ('ip-bytes-out', xlate_bytes)]
1259
1260 def format_stat(format, dict):
1261   if callable(format): return format(dict)
1262   else: return format % dict
1263
1264 ## How to lay out the stats dialog.  Format is (LABEL, FORMAT): LABEL is
1265 ## the label to give the entry box; FORMAT is the format string to write into
1266 ## the entry.
1267 cryptolayout = \
1268   [('Diffie-Hellman group',
1269     '%(kx-group)s '
1270     '(%(kx-group-order-bits)s-bit order, '
1271     '%(kx-group-elt-bits)s-bit elements)'),
1272    ('Bulk crypto transform',
1273     '%(bulk-transform)s (%(bulk-overhead)s byte overhead)'),
1274    ('Data encryption', lambda d: '%s (%s; %s)' % (
1275      d['cipher'],
1276      '%d-bit key' % (8*int(d['cipher-keysz'])),
1277      d.get('cipher-blksz', '0') == '0'
1278        and 'stream cipher'
1279        or '%d-bit block' % (8*int(d['cipher-blksz'])))),
1280    ('Message authentication', lambda d: '%s (%s; %s)' % (
1281      d['mac'],
1282      d.get('mac-keysz') is None
1283        and 'one-time MAC'
1284        or '%d-bit key' % (8*int(d['mac-keysz'])),
1285      '%d-bit tag' % (8*int(d['mac-tagsz'])))),
1286    ('Hash', lambda d: '%s (%d-bit output)' %
1287     (d['hash'], 8*int(d['hash-sz'])))]
1288
1289 statslayout = \
1290   [('Start time', '%(start-time)s'),
1291    ('Private key', '%(current-key)s')] + \
1292   cryptolayout + \
1293   [('Last key-exchange', '%(last-keyexch-time)s'),
1294    ('Last packet', '%(last-packet-time)s'),
1295    ('Packets in/out',
1296     '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1297    ('Key-exchange in/out',
1298     '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1299    ('IP in/out',
1300     '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1301    ('Rejected packets', '%(rejected-packets)s')]
1302
1303 class PeerWindow (TrivialWindow):
1304   """
1305   Show information about a peer.
1306
1307   This gives a graphical view of the server's peer statistics.
1308
1309   Interesting attributes:
1310
1311     * e: dict mapping keys (mostly matching label widget texts, though pings
1312       use command names) to entry widgets so that we can update them easily
1313     * peer: the peer this window shows information about
1314     * cr: the info-fetching coroutine, or None if crrrently disconnected
1315     * doupate: whether the info-fetching corouting should continue running
1316   """
1317
1318   def __init__(me, peer):
1319     """Construct a PeerWindow, showing information about PEER."""
1320
1321     TrivialWindow.__init__(me)
1322     me.set_title('TrIPE statistics: %s' % peer.name)
1323     me.peer = peer
1324
1325     table = GridPacker()
1326     me.add(table)
1327
1328     ## Utility for adding fields.
1329     me.e = {}
1330     def add(label, text = None, key = None):
1331       if key is None: key = label
1332       me.e[key] = table.info(label, text, len = 42, newlinep = True)
1333
1334     ## Build the dialogue box.
1335     add('Peer name', peer.name)
1336     add('Tunnel', peer.tunnel)
1337     add('Interface', peer.ifname)
1338     add('Keepalives',
1339         (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1340     add('Address', peer.addr)
1341     add('Transport pings', key = 'PING')
1342     add('Encrypted pings', key = 'EPING')
1343
1344     for label, format in statslayout:
1345       add(label)
1346
1347     ## Hook onto various interesting events.
1348     me.hook(conn.connecthook, me.tryupdate)
1349     me.hook(conn.disconnecthook, me.stopupdate)
1350     me.hook(me.closehook, me.stopupdate)
1351     me.hook(me.peer.deadhook, me.dead)
1352     me.hook(me.peer.changehook, me.change)
1353     me.hook(me.peer.pinghook, me.ping)
1354     me.cr = None
1355     me.doupdate = True
1356     me.tryupdate()
1357
1358     ## Format the ping statistics.
1359     for cmd, ps in me.peer.ping.iteritems():
1360       me.ping(me.peer, cmd, ps)
1361
1362     ## And show the window.
1363     me.show_all()
1364
1365   def change(me):
1366     """Update the display in response to a notification."""
1367     me.e['Interface'].set_text(me.peer.ifname)
1368     me.e['Address'].set_text(me.peer.addr)
1369
1370   def _update(me):
1371     """
1372     Main display-updating coroutine.
1373
1374     This does an update, sleeps for a while, and starts again.  If the
1375     me.doupdate flag goes low, we stop the loop.
1376     """
1377     while me.peer.alivep and conn.connectedp() and me.doupdate:
1378       stat = conn.stats(me.peer.name)
1379       for s, trans in statsxlate:
1380         stat[s] = trans(stat[s])
1381       stat.update(me.peer.__dict__)
1382       for label, format in statslayout:
1383         me.e[label].set_text(format_stat(format, stat))
1384       GL.timeout_add(1000, lambda: me.cr.switch() and False)
1385       me.cr.parent.switch()
1386     me.cr = None
1387
1388   def tryupdate(me):
1389     """Start the updater coroutine, if it's not going already."""
1390     if me.cr is None:
1391       me.cr = T.Coroutine(me._update,
1392                           name = 'update-peer-window %s' % me.peer.name)
1393       me.cr.switch()
1394
1395   def stopupdate(me, *hunoz, **hukairz):
1396     """Stop the update coroutine, by setting me.doupdate."""
1397     me.doupdate = False
1398
1399   def dead(me):
1400     """Called when the peer is killed."""
1401     me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1402     me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1403     me.stopupdate()
1404
1405   def ping(me, peer, cmd, ps):
1406     """Called when a ping result for the peer is reported."""
1407     s = '%d/%d' % (ps.ngood, ps.n)
1408     if ps.n:
1409       s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1410     if ps.ngood:
1411       s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1412     me.e[ps.command].set_text(s)
1413
1414 ###--------------------------------------------------------------------------
1415 ### Cryptographic status.
1416
1417 class CryptoInfo (TrivialWindow):
1418   """Simple display of cryptographic algorithms in use."""
1419   def __init__(me):
1420     TrivialWindow.__init__(me)
1421     me.set_title('Cryptographic algorithms')
1422     T.aside(me.populate)
1423   def populate(me):
1424     table = GridPacker()
1425     me.add(table)
1426
1427     crypto = conn.algs()
1428     firstp = True
1429     for label, format in cryptolayout:
1430       table.info(label, format_stat(format, crypto),
1431                  len = 42, newlinep = not firstp)
1432       firstp = False
1433
1434     me.show_all()
1435
1436 ###--------------------------------------------------------------------------
1437 ### Main monitor window.
1438
1439 class MonitorWindow (MyWindow):
1440
1441   """
1442   The main monitor window.
1443
1444   This class creates, populates and maintains the main monitor window.
1445
1446   Lots of attributes:
1447
1448     * warnings, trace: log models for server output
1449     * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1450       WindowSlot objects for ancillary windows
1451     * ui: Gtk UIManager object for the menu system
1452     * apmenu: pair of identical autoconnecting peer menus
1453     * listmodel: Gtk ListStore for connected peers; contains peer name,
1454       address, and ping times (transport and encrypted, value and colour)
1455     * status: Gtk Statusbar at the bottom of the window
1456     * _kidding: an unpleasant backchannel between the apchange method (which
1457       builds the apmenus) and the menu handler, forced on us by a Gtk
1458       misfeature
1459
1460   Also installs attributes on Peer objects:
1461
1462     * i: index of peer's entry in listmodel
1463     * win: WindowSlot object for the peer's PeerWindow
1464   """
1465
1466   def __init__(me):
1467     """Construct the window."""
1468
1469     ## Basic stuff.
1470     MyWindow.__init__(me)
1471     me.set_title('TrIPE monitor')
1472
1473     ## Hook onto diagnostic outputs.
1474     me.warnings = WarningLogModel()
1475     me.hook(conn.warnhook, me.warnings.notify)
1476     me.trace = TraceLogModel()
1477     me.hook(conn.tracehook, me.trace.notify)
1478
1479     ## Make slots to store the various ancillary singleton windows.
1480     me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1481     me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1482     me.traceopts = WindowSlot(lambda: TraceOptions())
1483     me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1484     me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1485     me.servinfo = WindowSlot(lambda: ServInfo())
1486
1487     ## Main window structure.
1488     vbox = G.VBox()
1489     me.add(vbox)
1490
1491     ## UI manager  makes our menus.  (We're too cheap to have a toolbar.)
1492     me.ui = G.UIManager()
1493     actgroup = makeactiongroup('monitor',
1494       [('file-menu', '_File', None, None),
1495        ('connect', '_Connect', '<Control>C', conn.connect),
1496        ('disconnect', '_Disconnect', '<Control>D',
1497         lambda: conn.disconnect(None)),
1498        ('quit', '_Quit', '<Control>Q', me.close),
1499        ('server-menu', '_Server', None, None),
1500        ('daemon', 'Run in _background', None, cr(conn.daemon)),
1501        ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1502        ('crypto-algs', 'Cryptographic algorithms',
1503         '<Control>Y', me.cryptoinfo.open),
1504        ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1505        ('server-quit', 'Terminate server', None, cr(conn.quit)),
1506        ('conn-peer', 'Connect peer', None, None),
1507        ('logs-menu', '_Logs', None, None),
1508        ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1509        ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1510        ('trace-options', 'Trace _options...', None, me.traceopts.open),
1511        ('help-menu', '_Help', None, None),
1512        ('about', '_About tripemon...', None, aboutbox.open),
1513        ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1514        ('kill-peer', '_Kill peer', None, me.killpeer),
1515        ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1516
1517     ## Menu structures.
1518     uidef = '''
1519       <ui>
1520         <menubar>
1521           <menu action="file-menu">
1522             <menuitem action="quit"/>
1523           </menu>
1524           <menu action="server-menu">
1525             <menuitem action="connect"/>
1526             <menuitem action="disconnect"/>
1527             <separator/>
1528             <menuitem action="server-version"/>
1529             <menuitem action="crypto-algs"/>
1530             <menuitem action="add-peer"/>
1531             <menuitem action="conn-peer"/>
1532             <menuitem action="daemon"/>
1533             <menuitem action="reload-keys"/>
1534             <separator/>
1535             <menuitem action="server-quit"/>
1536           </menu>
1537           <menu action="logs-menu">
1538             <menuitem action="show-warnings"/>
1539             <menuitem action="show-trace"/>
1540             <menuitem action="trace-options"/>
1541           </menu>
1542           <menu action="help-menu">
1543             <menuitem action="about"/>
1544           </menu>
1545         </menubar>
1546         <popup name="peer-popup">
1547           <menuitem action="add-peer"/>
1548           <menuitem action="conn-peer"/>
1549           <menuitem action="kill-peer"/>
1550           <menuitem action="force-kx"/>
1551         </popup>
1552       </ui>
1553       '''
1554
1555     ## Populate the UI manager.
1556     me.ui.insert_action_group(actgroup, 0)
1557     me.ui.add_ui_from_string(uidef)
1558
1559     ## Construct the menu bar.
1560     vbox.pack_start(me.ui.get_widget('/menubar'), False, True, 0)
1561     me.add_accel_group(me.ui.get_accel_group())
1562
1563     ## Construct and attach the auto-peers menu.  (This is a horrible bodge
1564     ## because we can't attach the same submenu in two different places.)
1565     me.apmenu = G.Menu(), G.Menu()
1566     me.ui.get_widget('/menubar/server-menu/conn-peer') \
1567       .set_submenu(me.apmenu[0])
1568     me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1569
1570     ## Construct the main list model, and listen on hooks which report
1571     ## changes to the available peers.
1572     me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1573     me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1574     me.hook(monitor.peers.addhook, me.addpeer)
1575     me.hook(monitor.peers.delhook, me.delpeer)
1576     me.hook(monitor.autopeershook, me.apchange)
1577
1578     ## Construct the list viewer and put it in a scrolling window.
1579     scr = MyScrolledWindow()
1580     me.list = MyTreeView(me.listmodel)
1581     me.list.append_column(G.TreeViewColumn('Peer name',
1582                                            G.CellRendererText(),
1583                                            text = 0))
1584     me.list.append_column(G.TreeViewColumn('Address',
1585                                            G.CellRendererText(),
1586                                            text = 1))
1587     me.list.append_column(G.TreeViewColumn('T-ping',
1588                                            G.CellRendererText(),
1589                                            text = 2,
1590                                            foreground = 3))
1591     me.list.append_column(G.TreeViewColumn('E-ping',
1592                                            G.CellRendererText(),
1593                                            text = 4,
1594                                            foreground = 5))
1595     me.list.get_column(1).set_expand(True)
1596     me.list.connect('row-activated', me.activate)
1597     me.list.connect('button-press-event', me.buttonpress)
1598     me.list.set_reorderable(True)
1599     me.list.get_selection().set_mode(G.SELECTION_NONE)
1600     scr.add(me.list)
1601     vbox.pack_start(scr, True, True, 0)
1602
1603     ## Construct the status bar, and listen on hooks which report changes to
1604     ## connection status.
1605     me.status = G.Statusbar()
1606     vbox.pack_start(me.status, False, True, 0)
1607     me.hook(conn.connecthook, cr(me.connected))
1608     me.hook(conn.disconnecthook, me.disconnected)
1609     me.hook(conn.notehook, me.notify)
1610
1611     ## Set a plausible default window size.
1612     me.set_default_size(512, 180)
1613
1614   def addpeer(me, peer):
1615     """Hook: announces that PEER has been added."""
1616     peer.i = me.listmodel.append([peer.name, peer.addr,
1617                                   '???', 'green', '???', 'green'])
1618     peer.win = WindowSlot(lambda: PeerWindow(peer))
1619     me.hook(peer.pinghook, me._ping)
1620     me.hook(peer.changehook, lambda: me._change(peer))
1621     me.apchange()
1622
1623   def delpeer(me, peer):
1624     """Hook: announces that PEER has been removed."""
1625     me.listmodel.remove(peer.i)
1626     me.unhook(peer.pinghook)
1627     me.apchange()
1628
1629   def path_peer(me, path):
1630     """Return the peer corresponding to a given list-model PATH."""
1631     return monitor.peers[me.listmodel[path][0]]
1632
1633   def apchange(me):
1634     """
1635     Hook: announces that a change has been made to the peers available for
1636     automated connection.
1637
1638     This populates both auto-peer menus and keeps them in sync.  (As
1639     mentioned above, we can't attach the same submenu to two separate parent
1640     menu items.  So we end up with two identical menus instead.  Yes, this
1641     does suck.)
1642     """
1643
1644     ## The set_active method of a CheckMenuItem works by maybe activating the
1645     ## menu item.  This signals our handler.  But we don't actually want to
1646     ## signal the handler unless the user actually frobbed the item.  So the
1647     ## _kidding flag is used as an underhanded way of telling the handler
1648     ## that we don't actually want it to do anything.  Of course, this sucks
1649     ## mightily.
1650     me._kidding = True
1651
1652     ## Iterate over the two menus.
1653     for m in 0, 1:
1654       menu = me.apmenu[m]
1655       existing = menu.get_children()
1656       if monitor.autopeers is None:
1657
1658         ## No peers, so empty out the menu.
1659         for item in existing:
1660           menu.remove(item)
1661
1662       else:
1663
1664         ## Insert the new items into the menu.  (XXX this seems buggy XXX)
1665         ## Tick the peers which are actually connected.
1666         i = j = 0
1667         for peer in monitor.autopeers:
1668           if j < len(existing) and \
1669              existing[j].get_child().get_text() == peer:
1670             item = existing[j]
1671             j += 1
1672           else:
1673             item = G.CheckMenuItem(peer, use_underline = False)
1674             item.connect('activate', invoker(me._addautopeer, peer))
1675             menu.insert(item, i)
1676           item.set_active(peer in monitor.peers.table)
1677           i += 1
1678
1679       ## Make all the menu items visible.
1680       menu.show_all()
1681
1682     ## Set the parent menu items sensitive if and only if there are any peers
1683     ## to connect.
1684     for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1685       me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1686
1687     ## And now allow the handler to do its business normally.
1688     me._kidding = False
1689
1690   def _addautopeer(me, peer):
1691     """
1692     Automatically connect an auto-peer.
1693
1694     This method is invoked from the main coroutine.  Since the actual
1695     connection needs to issue administration commands, we must spawn a new
1696     child coroutine for it.
1697     """
1698     if me._kidding:
1699       return
1700     T.Coroutine(me._addautopeer_hack,
1701                 name = '_addautopeerhack %s' % peer).switch(peer)
1702
1703   def _addautopeer_hack(me, peer):
1704     """Make an automated connection to PEER in response to a user click."""
1705     if me._kidding:
1706       return
1707     try:
1708       T._simple(conn.svcsubmit('connect', 'active', peer))
1709     except T.TripeError, exc:
1710       T.defer(moanbox, ' '.join(exc.args))
1711     me.apchange()
1712
1713   def activate(me, l, path, col):
1714     """
1715     Handle a double-click on a peer in the main list: open a PeerInfo window.
1716     """
1717     peer = me.path_peer(path)
1718     peer.win.open()
1719
1720   def buttonpress(me, l, ev):
1721     """
1722     Handle a mouse click on the main list.
1723
1724     Currently we're only interested in button-3, which pops up the peer menu.
1725     For future reference, we stash the peer that was clicked in me.menupeer.
1726     """
1727     if ev.button == 3:
1728       x, y = int(ev.x), int(ev.y)
1729       r = me.list.get_path_at_pos(x, y)
1730       for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1731         me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1732                                           r is not None)
1733       me.ui.get_widget('/peer-popup/conn-peer'). \
1734         set_sensitive(bool(monitor.autopeers))
1735       if r:
1736         me.menupeer = me.path_peer(r[0])
1737       else:
1738         me.menupeer = None
1739       me.ui.get_widget('/peer-popup').popup(
1740         None, None, None, ev.button, ev.time)
1741
1742   def killpeer(me):
1743     """Kill a peer from the popup menu."""
1744     cr(conn.kill, me.menupeer.name)()
1745
1746   def forcekx(me):
1747     """Kickstart a key-exchange from the popup menu."""
1748     cr(conn.forcekx, me.menupeer.name)()
1749
1750   _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1751   def _ping(me, p, cmd, ps):
1752     """Hook: responds to ping reports."""
1753     textcol, colourcol = me._columnmap[cmd]
1754     if ps.nmissrun:
1755       me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1756       me.listmodel[p.i][colourcol] = 'red'
1757     else:
1758       me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1759       me.listmodel[p.i][colourcol] = 'black'
1760
1761   def _change(me, p):
1762     """Hook: notified when the peer changes state."""
1763     me.listmodel[p.i][1] = p.addr
1764
1765   def setstatus(me, status):
1766     """Update the message in the status bar."""
1767     me.status.pop(0)
1768     me.status.push(0, status)
1769
1770   def notify(me, note, *rest):
1771     """Hook: invoked when interesting notifications occur."""
1772     if note == 'DAEMON':
1773       me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1774
1775   def connected(me):
1776     """
1777     Hook: invoked when a connection is made to the server.
1778
1779     Make options which require a server connection sensitive.
1780     """
1781     me.setstatus('Connected (port %s)' % conn.port())
1782     me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1783     for i in ('/menubar/server-menu/disconnect',
1784               '/menubar/server-menu/server-version',
1785               '/menubar/server-menu/add-peer',
1786               '/menubar/server-menu/server-quit',
1787               '/menubar/logs-menu/trace-options'):
1788       me.ui.get_widget(i).set_sensitive(True)
1789     me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1790       set_sensitive(bool(monitor.autopeers))
1791     me.ui.get_widget('/menubar/server-menu/daemon'). \
1792       set_sensitive(conn.servinfo()['daemon'] == 'nil')
1793
1794   def disconnected(me, reason):
1795     """
1796     Hook: invoked when the connection to the server is lost.
1797
1798     Make most options insensitive.
1799     """
1800     me.setstatus('Disconnected')
1801     me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1802     for i in ('/menubar/server-menu/disconnect',
1803               '/menubar/server-menu/server-version',
1804               '/menubar/server-menu/add-peer',
1805               '/menubar/server-menu/conn-peer',
1806               '/menubar/server-menu/daemon',
1807               '/menubar/server-menu/server-quit',
1808               '/menubar/logs-menu/trace-options'):
1809       me.ui.get_widget(i).set_sensitive(False)
1810     if reason: moanbox(reason)
1811
1812 ###--------------------------------------------------------------------------
1813 ### Main program.
1814
1815 def parse_options():
1816   """
1817   Parse command-line options.
1818
1819   Process the boring ones.  Return all of them, for later.
1820   """
1821   op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1822                     version = '%prog (tripe version 1.0.0)')
1823   op.add_option('-a', '--admin-socket',
1824                 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1825                 help = 'Select socket to connect to [default %default]')
1826   op.add_option('-d', '--directory',
1827                 metavar = 'DIR', dest = 'dir', default = T.configdir,
1828                 help = 'Select current diretory [default %default]')
1829   opts, args = op.parse_args()
1830   if args: op.error('no arguments permitted')
1831   OS.chdir(opts.dir)
1832   return opts
1833
1834 def init(opts):
1835   """Initialization."""
1836
1837   global conn, monitor, pinger
1838
1839   ## Try to establish a connection.
1840   conn = Connection(opts.tripesock)
1841
1842   ## Make the main interesting coroutines and objects.
1843   monitor = Monitor()
1844   pinger = Pinger()
1845   pinger.switch()
1846
1847 def main():
1848
1849   ## Main window.
1850   root = MonitorWindow()
1851   conn.connect()
1852   root.show_all()
1853
1854   ## Main loop.
1855   HookClient().hook(root.closehook, exit)
1856   conn.mainloop()
1857
1858 if __name__ == '__main__':
1859   opts = parse_options()
1860   init(opts)
1861   main()
1862
1863 ###----- That's all, folks --------------------------------------------------