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