chiark / gitweb /
mon/tripemon.in: Show per-peer crypto details in peer info sheet.
[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.c_keepalive = G.CheckButton('Keepalives')
1069     me.l_tunnel = table.labelled('Tunnel', combo_box_text(),
1070                                  newlinep = True, width = 3)
1071     me.tuns = conn.tunnels()
1072     for t in me.tuns:
1073       me.l_tunnel.append_text(t)
1074     me.l_tunnel.set_active(0)
1075     table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1076     me.c_keepalive.connect('toggled',
1077                            lambda t: me.e_keepalive.set_sensitive\
1078                                       (t.get_active()))
1079     me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1080     me.e_keepalive.set_sensitive(False)
1081     table.pack(me.e_keepalive, width = 3)
1082     me.show_all()
1083
1084   def ok(me):
1085     """Handle an OK press: create the peer."""
1086     try:
1087       if me.c_keepalive.get_active():
1088         ka = me.e_keepalive.get_text()
1089       else:
1090         ka = None
1091       t = me.l_tunnel.get_active()
1092       if t == 0:
1093         tun = None
1094       else:
1095         tun = me.tuns[t]
1096         me._addpeer(me.e_name.get_text(),
1097                     me.e_addr.get_text(),
1098                     me.e_port.get_text(),
1099                     ka,
1100                     tun)
1101     except ValidationError:
1102       GDK.beep()
1103       return
1104
1105   @incr
1106   def _addpeer(me, name, addr, port, keepalive, tunnel):
1107     """Coroutine function: actually do the ADD command."""
1108     try:
1109       conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel)
1110       me.destroy()
1111     except T.TripeError, exc:
1112       T.defer(moanbox, ' '.join(exc))
1113
1114 class ServInfo (TrivialWindow):
1115   """
1116   Show information about the server and available services.
1117
1118   Interesting attributes:
1119
1120     * e: maps SERVINFO keys to entry widgets
1121     * svcs: Gtk ListStore describing services (columns are name and version)
1122   """
1123
1124   def __init__(me):
1125     TrivialWindow.__init__(me)
1126     me.set_title('TrIPE server info')
1127     table = GridPacker()
1128     me.add(table)
1129     me.e = {}
1130     def add(label, tag, text = None, **kw):
1131       me.e[tag] = table.info(label, text, **kw)
1132     add('Implementation', 'implementation')
1133     add('Version', 'version', newlinep = True)
1134     me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1135     me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1136     scr = MyScrolledWindow()
1137     lb = MyTreeView(me.svcs)
1138     i = 0
1139     for title in 'Service', 'Version':
1140       lb.append_column(G.TreeViewColumn(
1141         title, G.CellRendererText(), text = i))
1142       i += 1
1143     for svc in monitor.services:
1144       me.svcs.append([svc.name, svc.version])
1145     scr.add(lb)
1146     table.pack(scr, width = 2, newlinep = True,
1147                yopt = G.EXPAND | G.FILL | G.SHRINK)
1148     me.update()
1149     me.hook(conn.connecthook, me.update)
1150     me.hook(monitor.services.addhook, me.addsvc)
1151     me.hook(monitor.services.delhook, me.delsvc)
1152     me.show_all()
1153
1154   def addsvc(me, svc):
1155     me.svcs.append([svc.name, svc.version])
1156
1157   def delsvc(me, svc):
1158     for i in xrange(len(me.svcs)):
1159       if me.svcs[i][0] == svc.name:
1160         me.svcs.remove(me.svcs.get_iter(i))
1161         break
1162   @incr
1163   def update(me):
1164     info = conn.servinfo()
1165     for i in me.e:
1166       me.e[i].set_text(info[i])
1167
1168 class TraceOptions (MyDialog):
1169   """Tracing options window."""
1170   def __init__(me):
1171     MyDialog.__init__(me, title = 'Tracing options',
1172                       buttons = [(G.STOCK_CLOSE, me.destroy),
1173                                  (G.STOCK_OK, cr(me.ok))])
1174     me._setup()
1175
1176   @incr
1177   def _setup(me):
1178     me.opts = []
1179     for ch, st, desc in conn.trace():
1180       if ch.isupper(): continue
1181       text = desc[0].upper() + desc[1:]
1182       ticky = G.CheckButton(text)
1183       ticky.set_active(st == '+')
1184       me.vbox.pack_start(ticky, True, True, 0)
1185       me.opts.append((ch, ticky))
1186     me.show_all()
1187   def ok(me):
1188     on = []
1189     off = []
1190     for ch, ticky in me.opts:
1191       if ticky.get_active():
1192         on.append(ch)
1193       else:
1194         off.append(ch)
1195     setting = ''.join(on) + '-' + ''.join(off)
1196     conn.trace(setting)
1197     me.destroy()
1198
1199 ###--------------------------------------------------------------------------
1200 ### Peer window.
1201
1202 def xlate_time(t):
1203   """Translate a TrIPE-format time to something human-readable."""
1204   if t == 'NEVER': return '(never)'
1205   YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1206   ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1207   ago = MATH.floor(ago); unit = 's'
1208   for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1209     if ago < 2*n: break
1210     ago /= n
1211     unit = u
1212   return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1213          (YY, MM, DD, hh, mm, ss, ago, unit)
1214 def xlate_bytes(b):
1215   """Translate a number of bytes into something a human might want to read."""
1216   suff = 'B'
1217   b = int(b)
1218   for s in 'KMG':
1219     if b < 4096: break
1220     b /= 1024
1221     suff = s
1222   return '%d %s' % (b, suff)
1223
1224 ## How to translate peer stats.  Maps the stat name to a translation
1225 ## function.
1226 statsxlate = \
1227   [('start-time', xlate_time),
1228    ('last-packet-time', xlate_time),
1229    ('last-keyexch-time', xlate_time),
1230    ('bytes-in', xlate_bytes),
1231    ('bytes-out', xlate_bytes),
1232    ('keyexch-bytes-in', xlate_bytes),
1233    ('keyexch-bytes-out', xlate_bytes),
1234    ('ip-bytes-in', xlate_bytes),
1235    ('ip-bytes-out', xlate_bytes)]
1236
1237 ## How to lay out the stats dialog.  Format is (LABEL, FORMAT): LABEL is
1238 ## the label to give the entry box; FORMAT is the format string to write into
1239 ## the entry.
1240 statslayout = \
1241   [('Start time', '%(start-time)s'),
1242    ('Private key', '%(current-key)s'),
1243    ('Diffie-Hellman group',
1244     '%(kx-group)s '
1245     '(%(kx-group-order-bits)s-bit order, '
1246     '%(kx-group-elt-bits)s-bit elements)'),
1247    ('Cipher',
1248     '%(cipher)s (%(cipher-keysz)s-bit key, %(cipher-blksz)s-bit block)'),
1249    ('Mac', '%(mac)s (%(mac-keysz)s-bit key, %(mac-tagsz)s-bit tag)'),
1250    ('Hash', '%(hash)s (%(hash-sz)s-bit output)'),
1251    ('Last key-exchange', '%(last-keyexch-time)s'),
1252    ('Last packet', '%(last-packet-time)s'),
1253    ('Packets in/out',
1254     '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1255    ('Key-exchange in/out',
1256     '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1257    ('IP in/out',
1258     '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1259    ('Rejected packets', '%(rejected-packets)s')]
1260
1261 class PeerWindow (TrivialWindow):
1262   """
1263   Show information about a peer.
1264
1265   This gives a graphical view of the server's peer statistics.
1266
1267   Interesting attributes:
1268
1269     * e: dict mapping keys (mostly matching label widget texts, though pings
1270       use command names) to entry widgets so that we can update them easily
1271     * peer: the peer this window shows information about
1272     * cr: the info-fetching coroutine, or None if crrrently disconnected
1273     * doupate: whether the info-fetching corouting should continue running
1274   """
1275
1276   def __init__(me, peer):
1277     """Construct a PeerWindow, showing information about PEER."""
1278
1279     TrivialWindow.__init__(me)
1280     me.set_title('TrIPE statistics: %s' % peer.name)
1281     me.peer = peer
1282
1283     table = GridPacker()
1284     me.add(table)
1285
1286     ## Utility for adding fields.
1287     me.e = {}
1288     def add(label, text = None, key = None):
1289       if key is None: key = label
1290       me.e[key] = table.info(label, text, len = 42, newlinep = True)
1291
1292     ## Build the dialogue box.
1293     add('Peer name', peer.name)
1294     add('Tunnel', peer.tunnel)
1295     add('Interface', peer.ifname)
1296     add('Keepalives',
1297         (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1298     add('Address', peer.addr)
1299     add('Transport pings', key = 'PING')
1300     add('Encrypted pings', key = 'EPING')
1301
1302     for label, format in statslayout:
1303       add(label)
1304
1305     ## Hook onto various interesting events.
1306     me.hook(conn.connecthook, me.tryupdate)
1307     me.hook(conn.disconnecthook, me.stopupdate)
1308     me.hook(me.closehook, me.stopupdate)
1309     me.hook(me.peer.deadhook, me.dead)
1310     me.hook(me.peer.changehook, me.change)
1311     me.hook(me.peer.pinghook, me.ping)
1312     me.cr = None
1313     me.doupdate = True
1314     me.tryupdate()
1315
1316     ## Format the ping statistics.
1317     for cmd, ps in me.peer.ping.iteritems():
1318       me.ping(me.peer, cmd, ps)
1319
1320     ## And show the window.
1321     me.show_all()
1322
1323   def change(me):
1324     """Update the display in response to a notification."""
1325     me.e['Interface'].set_text(me.peer.ifname)
1326
1327   def _update(me):
1328     """
1329     Main display-updating coroutine.
1330
1331     This does an update, sleeps for a while, and starts again.  If the
1332     me.doupdate flag goes low, we stop the loop.
1333     """
1334     while me.peer.alivep and conn.connectedp() and me.doupdate:
1335       stat = conn.stats(me.peer.name)
1336       for s, trans in statsxlate:
1337         stat[s] = trans(stat[s])
1338       stat.update(me.peer.__dict__)
1339       for label, format in statslayout:
1340         me.e[label].set_text(format % stat)
1341       GL.timeout_add(1000, lambda: me.cr.switch() and False)
1342       me.cr.parent.switch()
1343     me.cr = None
1344
1345   def tryupdate(me):
1346     """Start the updater coroutine, if it's not going already."""
1347     if me.cr is None:
1348       me.cr = T.Coroutine(me._update,
1349                           name = 'update-peer-window %s' % me.peer.name)
1350       me.cr.switch()
1351
1352   def stopupdate(me, *hunoz, **hukairz):
1353     """Stop the update coroutine, by setting me.doupdate."""
1354     me.doupdate = False
1355
1356   def dead(me):
1357     """Called when the peer is killed."""
1358     me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1359     me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1360     me.stopupdate()
1361
1362   def ping(me, peer, cmd, ps):
1363     """Called when a ping result for the peer is reported."""
1364     s = '%d/%d' % (ps.ngood, ps.n)
1365     if ps.n:
1366       s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1367     if ps.ngood:
1368       s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1369     me.e[ps.command].set_text(s)
1370
1371 ###--------------------------------------------------------------------------
1372 ### Cryptographic status.
1373
1374 class CryptoInfo (TrivialWindow):
1375   """Simple display of cryptographic algorithms in use."""
1376   def __init__(me):
1377     TrivialWindow.__init__(me)
1378     me.set_title('Cryptographic algorithms')
1379     T.aside(me.populate)
1380   def populate(me):
1381     table = GridPacker()
1382     me.add(table)
1383
1384     crypto = conn.algs()
1385     table.info('Diffie-Hellman group',
1386                '%s (%d-bit order, %d-bit elements)' %
1387                (crypto['kx-group'],
1388                 int(crypto['kx-group-order-bits']),
1389                 int(crypto['kx-group-elt-bits'])),
1390                len = 32)
1391     table.info('Data encryption',
1392                '%s (%d-bit key; %s)' %
1393                (crypto['cipher'],
1394                 int(crypto['cipher-keysz']) * 8,
1395                 crypto['cipher-blksz'] == '0'
1396                   and 'stream cipher'
1397                   or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1398                newlinep = True)
1399     table.info('Message authentication',
1400                '%s (%d-bit key; %d-bit tag)' %
1401                (crypto['mac'],
1402                 int(crypto['mac-keysz']) * 8,
1403                 int(crypto['mac-tagsz']) * 8),
1404                newlinep = True)
1405     table.info('Hash function',
1406                '%s (%d-bit output)' %
1407                (crypto['hash'],
1408                 int(crypto['hash-sz']) * 8),
1409                newlinep = True)
1410
1411     me.show_all()
1412
1413 ###--------------------------------------------------------------------------
1414 ### Main monitor window.
1415
1416 class MonitorWindow (MyWindow):
1417
1418   """
1419   The main monitor window.
1420
1421   This class creates, populates and maintains the main monitor window.
1422
1423   Lots of attributes:
1424
1425     * warnings, trace: log models for server output
1426     * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1427       WindowSlot objects for ancillary windows
1428     * ui: Gtk UIManager object for the menu system
1429     * apmenu: pair of identical autoconnecting peer menus
1430     * listmodel: Gtk ListStore for connected peers; contains peer name,
1431       address, and ping times (transport and encrypted, value and colour)
1432     * status: Gtk Statusbar at the bottom of the window
1433     * _kidding: an unpleasant backchannel between the apchange method (which
1434       builds the apmenus) and the menu handler, forced on us by a Gtk
1435       misfeature
1436
1437   Also installs attributes on Peer objects:
1438
1439     * i: index of peer's entry in listmodel
1440     * win: WindowSlot object for the peer's PeerWindow
1441   """
1442
1443   def __init__(me):
1444     """Construct the window."""
1445
1446     ## Basic stuff.
1447     MyWindow.__init__(me)
1448     me.set_title('TrIPE monitor')
1449
1450     ## Hook onto diagnostic outputs.
1451     me.warnings = WarningLogModel()
1452     me.hook(conn.warnhook, me.warnings.notify)
1453     me.trace = TraceLogModel()
1454     me.hook(conn.tracehook, me.trace.notify)
1455
1456     ## Make slots to store the various ancillary singleton windows.
1457     me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1458     me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1459     me.traceopts = WindowSlot(lambda: TraceOptions())
1460     me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1461     me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1462     me.servinfo = WindowSlot(lambda: ServInfo())
1463
1464     ## Main window structure.
1465     vbox = G.VBox()
1466     me.add(vbox)
1467
1468     ## UI manager  makes our menus.  (We're too cheap to have a toolbar.)
1469     me.ui = G.UIManager()
1470     actgroup = makeactiongroup('monitor',
1471       [('file-menu', '_File', None, None),
1472        ('connect', '_Connect', '<Control>C', conn.connect),
1473        ('disconnect', '_Disconnect', '<Control>D',
1474         lambda: conn.disconnect(None)),
1475        ('quit', '_Quit', '<Control>Q', me.close),
1476        ('server-menu', '_Server', None, None),
1477        ('daemon', 'Run in _background', None, cr(conn.daemon)),
1478        ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1479        ('crypto-algs', 'Cryptographic algorithms',
1480         '<Control>Y', me.cryptoinfo.open),
1481        ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1482        ('server-quit', 'Terminate server', None, cr(conn.quit)),
1483        ('conn-peer', 'Connect peer', None, None),
1484        ('logs-menu', '_Logs', None, None),
1485        ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1486        ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1487        ('trace-options', 'Trace _options...', None, me.traceopts.open),
1488        ('help-menu', '_Help', None, None),
1489        ('about', '_About tripemon...', None, aboutbox.open),
1490        ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1491        ('kill-peer', '_Kill peer', None, me.killpeer),
1492        ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1493
1494     ## Menu structures.
1495     uidef = '''
1496       <ui>
1497         <menubar>
1498           <menu action="file-menu">
1499             <menuitem action="quit"/>
1500           </menu>
1501           <menu action="server-menu">
1502             <menuitem action="connect"/>
1503             <menuitem action="disconnect"/>
1504             <separator/>
1505             <menuitem action="server-version"/>
1506             <menuitem action="crypto-algs"/>
1507             <menuitem action="add-peer"/>
1508             <menuitem action="conn-peer"/>
1509             <menuitem action="daemon"/>
1510             <menuitem action="reload-keys"/>
1511             <separator/>
1512             <menuitem action="server-quit"/>
1513           </menu>
1514           <menu action="logs-menu">
1515             <menuitem action="show-warnings"/>
1516             <menuitem action="show-trace"/>
1517             <menuitem action="trace-options"/>
1518           </menu>
1519           <menu action="help-menu">
1520             <menuitem action="about"/>
1521           </menu>
1522         </menubar>
1523         <popup name="peer-popup">
1524           <menuitem action="add-peer"/>
1525           <menuitem action="conn-peer"/>
1526           <menuitem action="kill-peer"/>
1527           <menuitem action="force-kx"/>
1528         </popup>
1529       </ui>
1530       '''
1531
1532     ## Populate the UI manager.
1533     me.ui.insert_action_group(actgroup, 0)
1534     me.ui.add_ui_from_string(uidef)
1535
1536     ## Construct the menu bar.
1537     vbox.pack_start(me.ui.get_widget('/menubar'), False, True, 0)
1538     me.add_accel_group(me.ui.get_accel_group())
1539
1540     ## Construct and attach the auto-peers menu.  (This is a horrible bodge
1541     ## because we can't attach the same submenu in two different places.)
1542     me.apmenu = G.Menu(), G.Menu()
1543     me.ui.get_widget('/menubar/server-menu/conn-peer') \
1544       .set_submenu(me.apmenu[0])
1545     me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1546
1547     ## Construct the main list model, and listen on hooks which report
1548     ## changes to the available peers.
1549     me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1550     me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1551     me.hook(monitor.peers.addhook, me.addpeer)
1552     me.hook(monitor.peers.delhook, me.delpeer)
1553     me.hook(monitor.autopeershook, me.apchange)
1554
1555     ## Construct the list viewer and put it in a scrolling window.
1556     scr = MyScrolledWindow()
1557     me.list = MyTreeView(me.listmodel)
1558     me.list.append_column(G.TreeViewColumn('Peer name',
1559                                            G.CellRendererText(),
1560                                            text = 0))
1561     me.list.append_column(G.TreeViewColumn('Address',
1562                                            G.CellRendererText(),
1563                                            text = 1))
1564     me.list.append_column(G.TreeViewColumn('T-ping',
1565                                            G.CellRendererText(),
1566                                            text = 2,
1567                                            foreground = 3))
1568     me.list.append_column(G.TreeViewColumn('E-ping',
1569                                            G.CellRendererText(),
1570                                            text = 4,
1571                                            foreground = 5))
1572     me.list.get_column(1).set_expand(True)
1573     me.list.connect('row-activated', me.activate)
1574     me.list.connect('button-press-event', me.buttonpress)
1575     me.list.set_reorderable(True)
1576     me.list.get_selection().set_mode(G.SELECTION_NONE)
1577     scr.add(me.list)
1578     vbox.pack_start(scr, True, True, 0)
1579
1580     ## Construct the status bar, and listen on hooks which report changes to
1581     ## connection status.
1582     me.status = G.Statusbar()
1583     vbox.pack_start(me.status, False, True, 0)
1584     me.hook(conn.connecthook, cr(me.connected))
1585     me.hook(conn.disconnecthook, me.disconnected)
1586     me.hook(conn.notehook, me.notify)
1587
1588     ## Set a plausible default window size.
1589     me.set_default_size(512, 180)
1590
1591   def addpeer(me, peer):
1592     """Hook: announces that PEER has been added."""
1593     peer.i = me.listmodel.append([peer.name, peer.addr,
1594                                   '???', 'green', '???', 'green'])
1595     peer.win = WindowSlot(lambda: PeerWindow(peer))
1596     me.hook(peer.pinghook, me._ping)
1597     me.apchange()
1598
1599   def delpeer(me, peer):
1600     """Hook: announces that PEER has been removed."""
1601     me.listmodel.remove(peer.i)
1602     me.unhook(peer.pinghook)
1603     me.apchange()
1604
1605   def path_peer(me, path):
1606     """Return the peer corresponding to a given list-model PATH."""
1607     return monitor.peers[me.listmodel[path][0]]
1608
1609   def apchange(me):
1610     """
1611     Hook: announces that a change has been made to the peers available for
1612     automated connection.
1613
1614     This populates both auto-peer menus and keeps them in sync.  (As
1615     mentioned above, we can't attach the same submenu to two separate parent
1616     menu items.  So we end up with two identical menus instead.  Yes, this
1617     does suck.)
1618     """
1619
1620     ## The set_active method of a CheckMenuItem works by maybe activating the
1621     ## menu item.  This signals our handler.  But we don't actually want to
1622     ## signal the handler unless the user actually frobbed the item.  So the
1623     ## _kidding flag is used as an underhanded way of telling the handler
1624     ## that we don't actually want it to do anything.  Of course, this sucks
1625     ## mightily.
1626     me._kidding = True
1627
1628     ## Iterate over the two menus.
1629     for m in 0, 1:
1630       menu = me.apmenu[m]
1631       existing = menu.get_children()
1632       if monitor.autopeers is None:
1633
1634         ## No peers, so empty out the menu.
1635         for item in existing:
1636           menu.remove(item)
1637
1638       else:
1639
1640         ## Insert the new items into the menu.  (XXX this seems buggy XXX)
1641         ## Tick the peers which are actually connected.
1642         i = j = 0
1643         for peer in monitor.autopeers:
1644           if j < len(existing) and \
1645              existing[j].get_child().get_text() == peer:
1646             item = existing[j]
1647             j += 1
1648           else:
1649             item = G.CheckMenuItem(peer, use_underline = False)
1650             item.connect('activate', invoker(me._addautopeer, peer))
1651             menu.insert(item, i)
1652           item.set_active(peer in monitor.peers.table)
1653           i += 1
1654
1655       ## Make all the menu items visible.
1656       menu.show_all()
1657
1658     ## Set the parent menu items sensitive if and only if there are any peers
1659     ## to connect.
1660     for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1661       me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1662
1663     ## And now allow the handler to do its business normally.
1664     me._kidding = False
1665
1666   def _addautopeer(me, peer):
1667     """
1668     Automatically connect an auto-peer.
1669
1670     This method is invoked from the main coroutine.  Since the actual
1671     connection needs to issue administration commands, we must spawn a new
1672     child coroutine for it.
1673     """
1674     if me._kidding:
1675       return
1676     T.Coroutine(me._addautopeer_hack,
1677                 name = '_addautopeerhack %s' % peer).switch(peer)
1678
1679   def _addautopeer_hack(me, peer):
1680     """Make an automated connection to PEER in response to a user click."""
1681     if me._kidding:
1682       return
1683     try:
1684       T._simple(conn.svcsubmit('connect', 'active', peer))
1685     except T.TripeError, exc:
1686       T.defer(moanbox, ' '.join(exc.args))
1687     me.apchange()
1688
1689   def activate(me, l, path, col):
1690     """
1691     Handle a double-click on a peer in the main list: open a PeerInfo window.
1692     """
1693     peer = me.path_peer(path)
1694     peer.win.open()
1695
1696   def buttonpress(me, l, ev):
1697     """
1698     Handle a mouse click on the main list.
1699
1700     Currently we're only interested in button-3, which pops up the peer menu.
1701     For future reference, we stash the peer that was clicked in me.menupeer.
1702     """
1703     if ev.button == 3:
1704       x, y = int(ev.x), int(ev.y)
1705       r = me.list.get_path_at_pos(x, y)
1706       for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1707         me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1708                                           r is not None)
1709       me.ui.get_widget('/peer-popup/conn-peer'). \
1710         set_sensitive(bool(monitor.autopeers))
1711       if r:
1712         me.menupeer = me.path_peer(r[0])
1713       else:
1714         me.menupeer = None
1715       me.ui.get_widget('/peer-popup').popup(
1716         None, None, None, ev.button, ev.time)
1717
1718   def killpeer(me):
1719     """Kill a peer from the popup menu."""
1720     cr(conn.kill, me.menupeer.name)()
1721
1722   def forcekx(me):
1723     """Kickstart a key-exchange from the popup menu."""
1724     cr(conn.forcekx, me.menupeer.name)()
1725
1726   _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1727   def _ping(me, p, cmd, ps):
1728     """Hook: responds to ping reports."""
1729     textcol, colourcol = me._columnmap[cmd]
1730     if ps.nmissrun:
1731       me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1732       me.listmodel[p.i][colourcol] = 'red'
1733     else:
1734       me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1735       me.listmodel[p.i][colourcol] = 'black'
1736
1737   def setstatus(me, status):
1738     """Update the message in the status bar."""
1739     me.status.pop(0)
1740     me.status.push(0, status)
1741
1742   def notify(me, note, *rest):
1743     """Hook: invoked when interesting notifications occur."""
1744     if note == 'DAEMON':
1745       me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1746
1747   def connected(me):
1748     """
1749     Hook: invoked when a connection is made to the server.
1750
1751     Make options which require a server connection sensitive.
1752     """
1753     me.setstatus('Connected (port %s)' % conn.port())
1754     me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1755     for i in ('/menubar/server-menu/disconnect',
1756               '/menubar/server-menu/server-version',
1757               '/menubar/server-menu/add-peer',
1758               '/menubar/server-menu/server-quit',
1759               '/menubar/logs-menu/trace-options'):
1760       me.ui.get_widget(i).set_sensitive(True)
1761     me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1762       set_sensitive(bool(monitor.autopeers))
1763     me.ui.get_widget('/menubar/server-menu/daemon'). \
1764       set_sensitive(conn.servinfo()['daemon'] == 'nil')
1765
1766   def disconnected(me, reason):
1767     """
1768     Hook: invoked when the connection to the server is lost.
1769
1770     Make most options insensitive.
1771     """
1772     me.setstatus('Disconnected')
1773     me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1774     for i in ('/menubar/server-menu/disconnect',
1775               '/menubar/server-menu/server-version',
1776               '/menubar/server-menu/add-peer',
1777               '/menubar/server-menu/conn-peer',
1778               '/menubar/server-menu/daemon',
1779               '/menubar/server-menu/server-quit',
1780               '/menubar/logs-menu/trace-options'):
1781       me.ui.get_widget(i).set_sensitive(False)
1782     if reason: moanbox(reason)
1783
1784 ###--------------------------------------------------------------------------
1785 ### Main program.
1786
1787 def parse_options():
1788   """
1789   Parse command-line options.
1790
1791   Process the boring ones.  Return all of them, for later.
1792   """
1793   op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1794                     version = '%prog (tripe version 1.0.0)')
1795   op.add_option('-a', '--admin-socket',
1796                 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1797                 help = 'Select socket to connect to [default %default]')
1798   op.add_option('-d', '--directory',
1799                 metavar = 'DIR', dest = 'dir', default = T.configdir,
1800                 help = 'Select current diretory [default %default]')
1801   opts, args = op.parse_args()
1802   if args: op.error('no arguments permitted')
1803   OS.chdir(opts.dir)
1804   return opts
1805
1806 def init(opts):
1807   """Initialization."""
1808
1809   global conn, monitor, pinger
1810
1811   ## Try to establish a connection.
1812   conn = Connection(opts.tripesock)
1813
1814   ## Make the main interesting coroutines and objects.
1815   monitor = Monitor()
1816   pinger = Pinger()
1817   pinger.switch()
1818
1819 def main():
1820
1821   ## Main window.
1822   root = MonitorWindow()
1823   conn.connect()
1824   root.show_all()
1825
1826   ## Main loop.
1827   HookClient().hook(root.closehook, exit)
1828   conn.mainloop()
1829
1830 if __name__ == '__main__':
1831   opts = parse_options()
1832   init(opts)
1833   main()
1834
1835 ###----- That's all, folks --------------------------------------------------