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 --------------------------------------------------