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