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