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