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