#! @PYTHON@ # -*-python-*- #----- Dependencies --------------------------------------------------------- import socket as S from sys import argv, exit, stdin, stdout, stderr import os as OS from os import environ import math as M import sets as SET import getopt as O import time as T import re as RX from cStringIO import StringIO import pygtk pygtk.require('2.0') import gtk as G import gobject as GO import gtk.gdk as GDK #----- Configuration -------------------------------------------------------- tripedir = "@configdir@" socketdir = "@socketdir@" PACKAGE = "@PACKAGE@" VERSION = "@VERSION@" debug = False #----- Utility functions ---------------------------------------------------- ## Program name, shorn of extraneous stuff. quis = OS.path.basename(argv[0]) def moan(msg): """Report a message to standard error.""" stderr.write('%s: %s\n' % (quis, msg)) def die(msg, rc = 1): """Report a message to standard error and exit.""" moan(msg) exit(rc) rx_space = RX.compile(r'\s+') rx_ordinary = RX.compile(r'[^\\\'\"\s]+') rx_weird = RX.compile(r'([\\\'])') rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$') rx_num = RX.compile(r'^[-+]?\d+$') c_red = GDK.color_parse('red') def getword(s): """Pull a word from the front of S, handling quoting according to the tripe-admin(5) rules. Returns the word and the rest of S, or (None, None) if there are no more words left.""" i = 0 m = rx_space.match(s, i) if m: i = m.end() r = '' q = None if i >= len(s): return None, None while i < len(s) and (q or not s[i].isspace()): m = rx_ordinary.match(s, i) if m: r += m.group() i = m.end() elif s[i] == '\\': r += s[i + 1] i += 2 elif s[i] == q: q = None i += 1 elif not q and s[i] == '`' or s[i] == "'": q = "'" i += 1 elif not q and s[i] == '"': q = '"' i += 1 else: r += s[i] i += 1 if q: raise SyntaxError, 'missing close quote' m = rx_space.match(s, i) if m: i = m.end() return r, s[i:] def quotify(s): """Quote S according to the tripe-admin(5) rules.""" m = rx_ordinary.match(s) if m and m.end() == len(s): return s else: return "'" + rx_weird.sub(r'\\\1', s) + "'" #----- Random bits of infrastructure ---------------------------------------- class struct (object): """Simple object which stores attributes and has a sensible construction syntax.""" def __init__(me, **kw): me.__dict__.update(kw) class peerinfo (struct): pass class pingstate (struct): pass def invoker(func): """Return a function which throws away its arguments and calls FUNC. (If for loops worked by binding rather than assignment then we wouldn't need this kludge.""" return lambda *hunoz, **hukairz: func() class HookList (object): """I maintain a list of functions, and provide the ability to call them when something interesting happens. The functions are called in the order they were added to the list, with all the arguments. If a function returns a non-None result, no further functions are called.""" def __init__(me): me.list = [] def add(me, func, obj): me.list.append((obj, func)) def prune(me, obj): new = [] for o, f in me.list: if o is not obj: new.append((o, f)) me.list = new def run(me, *args, **kw): for o, hook in me.list: rc = hook(*args, **kw) if rc is not None: return rc return None class HookClient (object): def __init__(me): me.hooks = SET.Set() def hook(me, hk, func): hk.add(func, me) me.hooks.add(hk) def unhook(me, hk): hk.prune(me) me.hooks.discard(hk) def unhookall(me): for hk in me.hooks: hk.prune(me) me.hooks.clear() ##def __del__(me): ## print '%s dying' % me #----- Connections and commands --------------------------------------------- class ConnException (Exception): """Some sort of problem occurred while communicating with the tripe server.""" pass class Error (ConnException): """A command caused the server to issue a FAIL message.""" pass class ConnectionFailed (ConnException): """The connection failed while communicating with the server.""" jobid_seq = 0 def jobid(): """Return a job tag. Used for background commands.""" global jobid_seq jobid_seq += 1 return 'bg-%d' % jobid_seq class BackgroundCommand (HookClient): def __init__(me, conn, cmd): HookClient.__init__(me) me.conn = conn me.tag = None me.cmd = cmd me.donehook = HookList() me.losthook = HookList() me.info = [] me.submit() me.hook(me.conn.disconnecthook, me.lost) def submit(me): me.conn.bgcommand(me.cmd, me) def lost(me): me.losthook.run() me.unhookall() def fail(me, msg): me.conn.error("Unexpected error from server command `%s': %s" % (me.cmd % msg)) me.unhookall() def ok(me): me.donehook.run(me.info) me.unhookall() class SimpleBackgroundCommand (BackgroundCommand): def submit(me): try: BackgroundCommand.submit(me) except ConnectionFailed, err: me.conn.error('Unexpected error communicating with server: %s' % msg) raise class Connection (HookClient): """I represent a connection to the TrIPE server. I provide facilities for sending commands and receiving replies. The connection is notional: the underlying socket connection can come and go under our feet. Useful attributes: connectedp: whether the connection is active connecthook: called when we have connected disconnecthook: called if we have disconnected notehook: called with asynchronous notifications errorhook: called if there was a command error""" def __init__(me, sockname): """Make a new connection to the server listening to SOCKNAME. In fact, we're initially disconnected, to allow the caller to get his life in order before opening the floodgates.""" HookClient.__init__(me) me.sockname = sockname me.sock = None me.connectedp = False me.connecthook = HookList() me.disconnecthook = HookList() me.errorhook = HookList() me.inbuf = '' me.info = [] me.waitingp = False me.bgcmd = None me.bgmap = {} def connect(me): "Connect to the server. Runs connecthook if it works.""" if me.sock: return sock = S.socket(S.AF_UNIX, S.SOCK_STREAM) try: sock.connect(me.sockname) except S.error, err: me.error('error opening connection: %s' % err[1]) me.disconnecthook.run() return sock.setblocking(0) me.socketwatch = GO.io_add_watch(sock, GO.IO_IN, me.ready) me.sock = sock me.connectedp = True me.connecthook.run() def disconnect(me): "Disconnects from the server. Runs disconnecthook." if not me.sock: return GO.source_remove(me.socketwatch) me.sock.close() me.sock = None me.connectedp = False me.disconnecthook.run() def error(me, msg): """Reports an error on the connection.""" me.errorhook.run(msg) def bgcommand(me, cmd, bg): """Sends a background command and feeds it properly.""" try: me.bgcmd = bg err = me.docommand(cmd) if err: bg.fail(err) finally: me.bgcmd = None def command(me, cmd): """Sends a command to the server. Returns a list of INFO responses. Do not use this for backgrounded commands: create a BackgroundCommand instead. Raises apprpopriate exceptions on error, but doesn't send report them to the errorhook.""" err = me.docommand(cmd) if err: raise Error, err return me.info def docommand(me, cmd): if not me.sock: raise ConnException, 'not connected' if debug: print ">>> %s" % cmd me.sock.sendall(cmd + '\n') me.waitingp = True me.info = [] try: me.sock.setblocking(1) while True: rc, err = me.collect() if rc: break finally: me.waitingp = False me.sock.setblocking(0) if len(me.inbuf) > 0: GO.idle_add(lambda: me.flushbuf() and False) return err def simplecmd(me, cmd): """Like command(), but reports errors via the errorhook as well as raising exceptions.""" try: i = me.command(cmd) except Error, msg: me.error("Unexpected error from server command `%s': %s" % (cmd, msg)) raise except ConnectionFailed, msg: me.error("Unexpected error communicating with server: %s" % msg); raise return i def ready(me, sock, condition): try: me.collect() except ConnException, msg: me.error("Error watching server connection: %s" % msg) if me.sock: me.disconnect() me.connect() return True def collect(me): data = me.sock.recv(16384) if data == '': me.disconnect() raise ConnectionFailed, 'server disconnected' me.inbuf += data return me.flushbuf() def flushbuf(me): while True: nl = me.inbuf.find('\n') if nl < 0: break line = me.inbuf[:nl] if debug: print "<<< %s" % line me.inbuf = me.inbuf[nl + 1:] tag, line = getword(line) rc, err = me.parseline(tag, line) if rc: return rc, err return False, None def parseline(me, code, line): if code == 'BGDETACH': if not me.bgcmd: raise ConnectionFailed, 'unexpected detach' me.bgcmd.tag = line me.bgmap[line] = me.bgcmd me.waitingp = False me.bgcmd = None return True, None elif code == 'BGINFO': tag, line = getword(line) me.bgmap[tag].info.append(line) return False, None elif code == 'BGFAIL': tag, line = getword(line) me.bgmap[tag].fail(line) del me.bgmap[tag] return False, None elif code == 'BGOK': tag, line = getword(line) me.bgmap[tag].ok() del me.bgmap[tag] return False, None elif code == 'INFO': if not me.waitingp or me.bgcmd: raise ConnectionFailed, 'unexpected INFO response' me.info.append(line) return False, None elif code == 'OK': if not me.waitingp or me.bgcmd: raise ConnectionFailed, 'unexpected OK response' return True, None elif code == 'FAIL': if not me.waitingp: raise ConnectionFailed, 'unexpected FAIL response' return True, line else: raise ConnectionFailed, 'unknown response code `%s' % code class Monitor (Connection): """I monitor a TrIPE server, noticing when it changes state and keeping track of its peers. I also provide facilities for sending the server commands and collecting the answers. Useful attributes: addpeerhook: called with a new Peer when the server adds one delpeerhook: called with a Peer when the server kills one tracehook: called with a trace message warnhook: called with a warning message peers: mapping from names to Peer objects""" def __init__(me, sockname): """Initializes the monitor.""" Connection.__init__(me, sockname) me.addpeerhook = HookList() me.delpeerhook = HookList() me.tracehook = HookList() me.warnhook = HookList() me.notehook = HookList() me.hook(me.connecthook, me.connected) me.delay = [] me.peers = {} def addpeer(me, peer): if peer not in me.peers: p = Peer(me, peer) me.peers[peer] = p me.addpeerhook.run(p) def delpeer(me, peer): if peer in me.peers: p = me.peers[peer] me.delpeerhook.run(p) p.dead() del me.peers[peer] def updatelist(me, peers): newmap = {} for p in peers: newmap[p] = True if p not in me.peers: me.addpeer(p) oldpeers = me.peers.copy() for p in oldpeers: if p not in newmap: me.delpeer(p) def connected(me): try: me.simplecmd('WATCH -A+wnt') me.updatelist([s.strip() for s in me.simplecmd('LIST')]) except ConnException: me.disconnect() return def parseline(me, code, line): ## Delay async messages until the current command is done. Otherwise the ## handler for the async message might send another command before this ## one's complete, and the whole edifice turns to jelly. ## ## No, this isn't the server's fault. If we rely on the server to delay ## notifications then there's a race between when we send a command and ## when the server gets it. if me.waitingp and code in ('TRACE', 'WARN', 'NOTE'): if len(me.delay) == 0: GO.idle_add(me.flushdelay) me.delay.append((code, line)) elif code == 'TRACE': me.tracehook.run(line) elif code == 'WARN': me.warnhook.run(line) elif code == 'NOTE': note, line = getword(line) me.notehook.run(note, line) if note == 'ADD': me.addpeer(getword(line)[0]) elif note == 'KILL': me.delpeer(line) else: ## Well, I asked for it. pass else: return Connection.parseline(me, code, line) return False, None def flushdelay(me): delay = me.delay me.delay = [] for tag, line in delay: me.parseline(tag, line) return False def parseinfo(info): """Parse key=value output into a dictionary.""" d = {} for i in info: for w in i.split(' '): q = w.index('=') d[w[:q]] = w[q + 1:] return d class Peer (object): """I represent a TrIPE peer. Useful attributes are: name: peer's name addr: human-friendly representation of the peer's address ifname: interface associated with the peer alivep: true if the peer hasn't been killed deadhook: called with no arguments when the peer is killed""" def __init__(me, monitor, name): me.mon = monitor me.name = name addr = me.mon.simplecmd('ADDR %s' % name)[0].split(' ') if addr[0] == 'INET': ipaddr, port = addr[1:] try: name = S.gethostbyaddr(ipaddr)[0] me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr) except S.herror: me.addr = 'INET %s:%s' % (ipaddr, port) else: me.addr = ' '.join(addr) me.ifname = me.mon.simplecmd('IFNAME %s' % me.name)[0] me.__dict__.update(parseinfo(me.mon.simplecmd('PEERINFO %s' % me.name))) me.deadhook = HookList() me.alivep = True def dead(me): me.alivep = False me.deadhook.run() #----- Window management cruft ---------------------------------------------- class MyWindowMixin (G.Window, HookClient): """Mixin for windows which call a closehook when they're destroyed.""" def mywininit(me): me.closehook = HookList() HookClient.__init__(me) me.connect('destroy', invoker(me.close)) def close(me): me.closehook.run() me.destroy() me.unhookall() class MyWindow (MyWindowMixin): """A window which calls a closehook when it's destroyed.""" def __init__(me, kind = G.WINDOW_TOPLEVEL): G.Window.__init__(me, kind) me.mywininit() class MyDialog (G.Dialog, MyWindowMixin, HookClient): """A dialogue box with a closehook and sensible button binding.""" def __init__(me, title = None, flags = 0, buttons = []): """The buttons are a list of (STOCKID, THUNK) pairs: call the appropriate THUNK when the button is pressed. The others are just like GTK's Dialog class.""" i = 0 br = [] me.rmap = [] for b, f in buttons: br.append(b) br.append(i) me.rmap.append(f) i += 1 G.Dialog.__init__(me, title, None, flags, tuple(br)) HookClient.__init__(me) me.mywininit() me.set_default_response(i - 1) me.connect('response', me.respond) def respond(me, hunoz, rid, *hukairz): if rid >= 0: me.rmap[rid]() def makeactiongroup(name, acts): """Creates an ActionGroup called NAME. ACTS is a list of tuples containing: ACT: an action name LABEL: the label string for the action ACCEL: accelerator string, or None FUNC: thunk to call when the action is invoked""" actgroup = G.ActionGroup(name) for act, label, accel, func in acts: a = G.Action(act, label, None, None) if func: a.connect('activate', invoker(func)) actgroup.add_action_with_accel(a, accel) return actgroup class GridPacker (G.Table): """Like a Table, but with more state: makes filling in the widgets easier.""" def __init__(me): G.Table.__init__(me) me.row = 0 me.col = 0 me.rows = 1 me.cols = 1 me.set_border_width(4) me.set_col_spacings(4) me.set_row_spacings(4) def pack(me, w, width = 1, newlinep = False, xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0, xpad = 0, ypad = 0): """Packs a new widget. W is the widget to add. XOPY, YOPT, XPAD and YPAD are as for Table. WIDTH is how many cells to take up horizontally. NEWLINEP is whether to start a new line for this widget. Returns W.""" if newlinep: me.row += 1 me.col = 0 bot = me.row + 1 right = me.col + width if bot > me.rows or right > me.cols: if bot > me.rows: me.rows = bot if right > me.cols: me.cols = right me.resize(me.rows, me.cols) me.attach(w, me.col, me.col + width, me.row, me.row + 1, xopt, yopt, xpad, ypad) me.col += width return w def labelled(me, lab, w, newlinep = False, **kw): """Packs a labelled widget. Other arguments are as for pack. Returns W.""" label = G.Label(lab) label.set_alignment(1.0, 0) me.pack(label, newlinep = newlinep, xopt = G.FILL) me.pack(w, **kw) return w def info(me, label, text = None, len = 18, **kw): """Packs an information widget with a label. LABEL is the label; TEXT is the initial text; LEN is the estimated length in characters. Returns the entry widget.""" e = G.Entry() if text is not None: e.set_text(text) e.set_width_chars(len) e.set_editable(False) me.labelled(label, e, **kw) return e class WindowSlot (HookClient): """A place to store a window. If the window is destroyed, remember this; when we come to open the window, raise it if it already exists; otherwise make a new one.""" def __init__(me, createfunc): """Constructor: CREATEFUNC must return a new Window which supports the closehook protocol.""" HookClient.__init__(me) me.createfunc = createfunc me.window = None def open(me): """Opens the window, creating it if necessary.""" if me.window: me.window.window.raise_() else: me.window = me.createfunc() me.hook(me.window.closehook, me.closed) def closed(me): me.unhook(me.window.closehook) me.window = None class ValidationError (Exception): """Raised by ValidatingEntry.get_text() if the text isn't valid.""" pass class ValidatingEntry (G.Entry): """Like an Entry, but makes the text go red if the contents are invalid. If get_text is called, and the text is invalid, ValidationError is raised.""" def __init__(me, valid, text = '', size = -1, *arg, **kw): """Make an Entry. VALID is a regular expression or a predicate on strings. TEXT is the default text to insert. SIZE is the size of the box to set, in characters (ish). Other arguments are passed to Entry.""" G.Entry.__init__(me, *arg, **kw) me.connect("changed", me.check) if callable(valid): me.validate = valid else: me.validate = RX.compile(valid).match me.ensure_style() me.c_ok = me.get_style().text[G.STATE_NORMAL] me.c_bad = c_red if size != -1: me.set_width_chars(size) me.set_activates_default(True) me.set_text(text) me.check() def check(me, *hunoz): if me.validate(G.Entry.get_text(me)): me.validp = True me.modify_text(G.STATE_NORMAL, me.c_ok) else: me.validp = False me.modify_text(G.STATE_NORMAL, me.c_bad) def get_text(me): if not me.validp: raise ValidationError return G.Entry.get_text(me) def numericvalidate(min = None, max = None): """Validation function for numbers. Entry must consist of an optional sign followed by digits, and the resulting integer must be within the given bounds.""" return lambda x: (rx_num.match(x) and (min is None or long(x) >= min) and (max is None or long(x) <= max)) #----- Various minor dialog boxen ------------------------------------------- GPL = """This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.""" class AboutBox (G.AboutDialog, MyWindowMixin): """The program `About' box.""" def __init__(me): G.AboutDialog.__init__(me) me.mywininit() me.set_name('TrIPEmon') me.set_version(VERSION) me.set_license(GPL) me.set_authors(['Mark Wooding']) me.connect('unmap', invoker(me.close)) me.show() aboutbox = WindowSlot(AboutBox) def moanbox(msg): """Report an error message in a window.""" d = G.Dialog('Error from %s' % quis, flags = G.DIALOG_MODAL, buttons = ((G.STOCK_OK, G.RESPONSE_NONE))) label = G.Label(msg) label.set_padding(20, 20) d.vbox.pack_start(label) label.show() d.run() d.destroy() def unimplemented(*hunoz): """Indicator of laziness.""" moanbox("I've not written that bit yet.") class ServInfo (MyWindow): def __init__(me, monitor): MyWindow.__init__(me) me.set_title('TrIPE server info') me.mon = monitor me.table = GridPacker() me.add(me.table) me.e = {} def add(label, tag, text = None, **kw): me.e[tag] = me.table.info(label, text, **kw) add('Implementation', 'implementation') add('Version', 'version', newlinep = True) me.update() me.hook(me.mon.connecthook, me.update) me.show_all() def update(me): info = parseinfo(me.mon.simplecmd('SERVINFO')) for i in me.e: me.e[i].set_text(info[i]) class TraceOptions (MyDialog): """Tracing options window.""" def __init__(me, monitor): MyDialog.__init__(me, title = 'Tracing options', buttons = [(G.STOCK_CLOSE, me.destroy), (G.STOCK_OK, me.ok)]) me.mon = monitor me.opts = [] for o in me.mon.simplecmd('TRACE'): char = o[0] onp = o[1] text = o[3].upper() + o[4:] if char.isupper(): continue ticky = G.CheckButton(text) ticky.set_active(onp != ' ') me.vbox.pack_start(ticky) me.opts.append((char, ticky)) me.show_all() def ok(me): on = [] off = [] for char, ticky in me.opts: if ticky.get_active(): on.append(char) else: off.append(char) setting = ''.join(on) + '-' + ''.join(off) me.mon.simplecmd('TRACE %s' % setting) me.destroy() #----- Logging windows ------------------------------------------------------ class LogModel (G.ListStore): """A simple list of log messages.""" def __init__(me, columns): """Call with a list of column names. All must be strings. We add a time column to the left.""" me.cols = ('Time',) + columns G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols))) def add(me, *entries): """Adds a new log message, with a timestamp.""" now = T.strftime('%Y-%m-%d %H:%M:%S') me.append((now,) + entries) class TraceLogModel (LogModel): """Log model for trace messages.""" def __init__(me): LogModel.__init__(me, ('Message',)) def notify(me, line): """Call with a new trace message.""" me.add(line) class WarningLogModel (LogModel): """Log model for warnings. We split the category out into a separate column.""" def __init__(me): LogModel.__init__(me, ('Category', 'Message')) def notify(me, line): """Call with a new warning message.""" me.add(*getword(line)) class LogViewer (MyWindow): """Log viewer window. Nothing very exciting.""" def __init__(me, model): MyWindow.__init__(me) me.model = model scr = G.ScrolledWindow() scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC) me.list = G.TreeView(me.model) me.closehook = HookList() i = 0 for c in me.model.cols: me.list.append_column(G.TreeViewColumn(c, G.CellRendererText(), text = i)) i += 1 me.set_default_size(440, 256) scr.add(me.list) me.add(scr) me.show_all() #----- Peer window ---------------------------------------------------------- def xlate_time(t): """Translate a time in tripe's stats format to something a human might actually want to read.""" if t == 'NEVER': return '(never)' YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6)) ago = T.time() - T.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1)) ago = M.floor(ago); unit = 's' for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]: if ago < 2*n: break ago /= n unit = u return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \ (YY, MM, DD, hh, mm, ss, ago, unit) def xlate_bytes(b): """Translate a number of bytes into something a human might want to read.""" suff = 'B' b = int(b) for s in 'KMG': if b < 4096: break b /= 1024 suff = s return '%d %s' % (b, suff) ## How to translate peer stats. Maps the stat name to a translation ## function. statsxlate = \ [('start-time', xlate_time), ('last-packet-time', xlate_time), ('last-keyexch-time', xlate_time), ('bytes-in', xlate_bytes), ('bytes-out', xlate_bytes), ('keyexch-bytes-in', xlate_bytes), ('keyexch-bytes-out', xlate_bytes), ('ip-bytes-in', xlate_bytes), ('ip-bytes-out', xlate_bytes)] ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is ## the label to give the entry box; FORMAT is the format string to write into ## the entry. statslayout = \ [('Start time', '%(start-time)s'), ('Last key-exchange', '%(last-keyexch-time)s'), ('Last packet', '%(last-packet-time)s'), ('Packets in/out', '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'), ('Key-exchange in/out', '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'), ('IP in/out', '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-in)s (%(ip-bytes-in)s)'), ('Rejected packets', '%(rejected-packets)s')] class PeerWindow (MyWindow): """Show information about a peer.""" def __init__(me, monitor, peer): MyWindow.__init__(me) me.set_title('TrIPE statistics: %s' % peer.name) me.mon = monitor me.peer = peer table = GridPacker() me.add(table) me.e = {} def add(label, text = None): me.e[label] = table.info(label, text, len = 42, newlinep = True) add('Peer name', peer.name) add('Tunnel', peer.tunnel) add('Interface', peer.ifname) add('Keepalives', (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive) add('Address', peer.addr) add('Transport pings') add('Encrypted pings') for label, format in statslayout: add(label) me.timeout = None me.hook(me.mon.connecthook, me.tryupdate) me.hook(me.mon.disconnecthook, me.stopupdate) me.hook(me.closehook, me.stopupdate) me.hook(me.peer.deadhook, me.dead) me.hook(me.peer.pinghook, me.ping) me.tryupdate() me.ping() me.show_all() def update(me): if not me.peer.alivep or not me.mon.connectedp: return False stat = parseinfo(me.mon.simplecmd('STATS %s' % me.peer.name)) for s, trans in statsxlate: stat[s] = trans(stat[s]) for label, format in statslayout: me.e[label].set_text(format % stat) return True def tryupdate(me): if me.timeout is None and me.update(): me.timeout = GO.timeout_add(1000, me.update) def stopupdate(me): if me.timeout is not None: GO.source_remove(me.timeout) me.timeout = None def dead(me): me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name) me.e['Peer name'].set_text('%s [defunct]' % me.peer.name) me.stopupdate() def ping(me): for ping in me.peer.ping, me.peer.eping: s = '%d/%d' % (ping.ngood, ping.n) if ping.n: s += ' (%.1f%%)' % (ping.ngood * 100.0/ping.n) if ping.ngood: s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast); me.e[ping.cmd].set_text(s) #----- Add peer ------------------------------------------------------------- class AddPeerCommand (SimpleBackgroundCommand): def __init__(me, conn, dlg, name, addr, port, keepalive = None, tunnel = None): me.name = name me.addr = addr me.port = port me.keepalive = keepalive me.tunnel = tunnel cmd = StringIO() cmd.write('ADD %s' % name) cmd.write(' -background %s' % jobid()) if keepalive is not None: cmd.write(' -keepalive %s' % keepalive) if tunnel is not None: cmd.write(' -tunnel %s' % tunnel) cmd.write(' INET %s %s' % (addr, port)) SimpleBackgroundCommand.__init__(me, conn, cmd.getvalue()) me.hook(me.donehook, invoker(dlg.destroy)) def fail(me, err): token, msg = getword(str(err)) if token in ('resolve-error', 'resolver-timeout'): moanbox("Unable to resolve hostname `%s'" % me.addr) elif token == 'peer-create-fail': moanbox("Couldn't create new peer `%s'" % me.name) elif token == 'peer-exists': moanbox("Peer `%s' already exists" % me.name) else: moanbox("Unexpected error from server command `ADD': %s" % err) class AddPeerDialog (MyDialog): def __init__(me, monitor): MyDialog.__init__(me, 'Add peer', buttons = [(G.STOCK_CANCEL, me.destroy), (G.STOCK_OK, me.ok)]) me.mon = monitor table = GridPacker() me.vbox.pack_start(table) me.e_name = table.labelled('Name', ValidatingEntry(r'^[^\s.:]+$', '', 16), width = 3) me.e_addr = table.labelled('Address', ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24), newlinep = True) me.e_port = table.labelled('Port', ValidatingEntry(numericvalidate(0, 65535), '4070', 5)) me.c_keepalive = G.CheckButton('Keepalives') me.l_tunnel = table.labelled('Tunnel', G.combo_box_new_text(), newlinep = True, width = 3) me.tuns = me.mon.simplecmd('TUNNELS') for t in me.tuns: me.l_tunnel.append_text(t) me.l_tunnel.set_active(0) table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL) me.c_keepalive.connect('toggled', lambda t: me.e_keepalive.set_sensitive\ (t.get_active())) me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5) me.e_keepalive.set_sensitive(False) table.pack(me.e_keepalive, width = 3) me.show_all() def ok(me): try: if me.c_keepalive.get_active(): ka = me.e_keepalive.get_text() else: ka = None t = me.l_tunnel.get_active() if t == 0: tun = None else: tun = me.tuns[t] AddPeerCommand(me.mon, me, me.e_name.get_text(), me.e_addr.get_text(), me.e_port.get_text(), keepalive = ka, tunnel = tun) except ValidationError: GDK.beep() return #----- The server monitor --------------------------------------------------- class PingCommand (SimpleBackgroundCommand): def __init__(me, conn, cmd, peer, func): me.peer = peer me.func = func SimpleBackgroundCommand.__init__ \ (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name)) def ok(me): tok, rest = getword(me.info[0]) if tok == 'ping-ok': me.func(me.peer, float(rest)) else: me.func(me.peer, None) me.unhookall() def fail(me, err): me.unhookall() def lost(me): me.unhookall() class MonitorWindow (MyWindow): def __init__(me, monitor): MyWindow.__init__(me) me.set_title('TrIPE monitor') me.mon = monitor me.hook(me.mon.errorhook, me.report) me.warnings = WarningLogModel() me.hook(me.mon.warnhook, me.warnings.notify) me.trace = TraceLogModel() me.hook(me.mon.tracehook, me.trace.notify) me.warnview = WindowSlot(lambda: LogViewer(me.warnings)) me.traceview = WindowSlot(lambda: LogViewer(me.trace)) me.traceopts = WindowSlot(lambda: TraceOptions(me.mon)) me.addpeerwin = WindowSlot(lambda: AddPeerDialog(me.mon)) me.servinfo = WindowSlot(lambda: ServInfo(me.mon)) vbox = G.VBox() me.add(vbox) me.ui = G.UIManager() def cmd(c): return lambda: me.mon.simplecmd(c) actgroup = makeactiongroup('monitor', [('file-menu', '_File', None, None), ('connect', '_Connect', 'C', me.mon.connect), ('disconnect', '_Disconnect', 'D', me.mon.disconnect), ('quit', '_Quit', 'Q', me.close), ('server-menu', '_Server', None, None), ('daemon', 'Run in _background', None, cmd('DAEMON')), ('server-version', 'Server version', 'V', me.servinfo.open), ('reload-keys', 'Reload keys', 'R', cmd('RELOAD')), ('server-quit', 'Terminate server', None, cmd('QUIT')), ('logs-menu', '_Logs', None, None), ('show-warnings', 'Show _warnings', 'W', me.warnview.open), ('show-trace', 'Show _trace', 'T', me.traceview.open), ('trace-options', 'Trace _options...', None, me.traceopts.open), ('help-menu', '_Help', None, None), ('about', '_About tripemon...', None, aboutbox.open), ('add-peer', '_Add peer...', 'A', me.addpeerwin.open), ('kill-peer', '_Kill peer', None, me.killpeer), ('force-kx', 'Force key e_xchange', None, me.forcekx)]) uidef = ''' ''' me.ui.insert_action_group(actgroup, 0) me.ui.add_ui_from_string(uidef) vbox.pack_start(me.ui.get_widget('/menubar'), expand = False) me.add_accel_group(me.ui.get_accel_group()) me.status = G.Statusbar() me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6) me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING) me.hook(me.mon.addpeerhook, me.addpeer) me.hook(me.mon.delpeerhook, me.delpeer) scr = G.ScrolledWindow() scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC) me.list = G.TreeView(me.listmodel) me.list.append_column(G.TreeViewColumn('Peer name', G.CellRendererText(), text = 0)) me.list.append_column(G.TreeViewColumn('Address', G.CellRendererText(), text = 1)) me.list.append_column(G.TreeViewColumn('T-ping', G.CellRendererText(), text = 2, foreground = 3)) me.list.append_column(G.TreeViewColumn('E-ping', G.CellRendererText(), text = 4, foreground = 5)) me.list.get_column(1).set_expand(True) me.list.connect('row-activated', me.activate) me.list.connect('button-press-event', me.buttonpress) me.list.set_reorderable(True) me.list.get_selection().set_mode(G.SELECTION_NONE) scr.add(me.list) vbox.pack_start(scr) vbox.pack_start(me.status, expand = False) me.hook(me.mon.connecthook, me.connected) me.hook(me.mon.disconnecthook, me.disconnected) me.hook(me.mon.notehook, me.notify) me.pinger = None me.set_default_size(420, 180) me.mon.connect() me.show_all() def addpeer(me, peer): peer.i = me.listmodel.append([peer.name, peer.addr, '???', 'green', '???', 'green']) peer.win = WindowSlot(lambda: PeerWindow(me.mon, peer)) peer.pinghook = HookList() peer.ping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0, tlast = 0, ttot = 0, tcol = 2, ccol = 3, cmd = 'Transport pings') peer.eping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0, tlast = 0, ttot = 0, tcol = 4, ccol = 5, cmd = 'Encrypted pings') def delpeer(me, peer): me.listmodel.remove(peer.i) def path_peer(me, path): return me.mon.peers[me.listmodel[path][0]] def activate(me, l, path, col): peer = me.path_peer(path) peer.win.open() def buttonpress(me, l, ev): if ev.button == 3: r = me.list.get_path_at_pos(ev.x, ev.y) for i in '/peer-popup/kill-peer', '/peer-popup/force-kx': me.ui.get_widget(i).set_sensitive(me.mon.connectedp and r is not None) if r: me.menupeer = me.path_peer(r[0]) else: me.menupeer = None me.ui.get_widget('/peer-popup').popup(None, None, None, ev.button, ev.time) def killpeer(me): me.mon.simplecmd('KILL %s' % me.menupeer.name) def forcekx(me): me.mon.simplecmd('FORCEKX %s' % me.menupeer.name) def reping(me): if me.pinger is not None: GO.source_remove(me.pinger) me.pinger = GO.timeout_add(10000, me.ping) me.ping() def unping(me): if me.pinger is not None: GO.source_remove(me.pinger) me.pinger = None def ping(me): for name in me.mon.peers: p = me.mon.peers[name] PingCommand(me.mon, 'PING', p, lambda p, t: me.pong(p, p.ping, t)) PingCommand(me.mon, 'EPING', p, lambda p, t: me.pong(p, p.eping, t)) return True def pong(me, p, ping, t): ping.n += 1 if t is None: ping.nmiss += 1 ping.nmissrun += 1 me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun me.listmodel[p.i][ping.ccol] = 'red' else: ping.ngood += 1 ping.nmissrun = 0 ping.tlast = t ping.ttot += t me.listmodel[p.i][ping.tcol] = '%.1f ms' % t me.listmodel[p.i][ping.ccol] = 'black' p.pinghook.run() def setstatus(me, status): me.status.pop(0) me.status.push(0, status) def notify(me, note, rest): if note == 'DAEMON': me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False) def connected(me): me.setstatus('Connected (port %s)' % me.mon.simplecmd('PORT')[0]) me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False) for i in ('/menubar/server-menu/disconnect', '/menubar/server-menu/server-version', '/menubar/server-menu/add-peer', '/menubar/server-menu/server-quit', '/menubar/logs-menu/trace-options'): me.ui.get_widget(i).set_sensitive(True) me.ui.get_widget('/menubar/server-menu/daemon'). \ set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] == 'nil') me.reping() def disconnected(me): me.setstatus('Disconnected') me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True) for i in ('/menubar/server-menu/disconnect', '/menubar/server-menu/server-version', '/menubar/server-menu/add-peer', '/menubar/server-menu/daemon', '/menubar/server-menu/server-quit', '/menubar/logs-menu/trace-options'): me.ui.get_widget(i).set_sensitive(False) me.unping() def destroy(me): if me.pinger is not None: GO.source_remove(me.pinger) def report(me, msg): moanbox(msg) return True #----- Parse options -------------------------------------------------------- def version(fp = stdout): """Print the program's version number.""" fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION)) def usage(fp): """Print a brief usage message for the program.""" fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis) def main(): global tripedir if 'TRIPEDIR' in environ: tripedir = environ['TRIPEDIR'] tripesock = environ.get('TRIPESOCK', '%s/%s' % (socketdir, 'tripesock')) try: opts, args = O.getopt(argv[1:], 'hvud:a:', ['help', 'version', 'usage', 'directory=', 'admin-socket=']) except O.GetoptError, exc: moan(exc) usage(stderr) exit(1) for o, v in opts: if o in ('-h', '--help'): version(stdout) print usage(stdout) print """ Graphical monitor for TrIPE VPN. Options supported: -h, --help Show this help message. -v, --version Show the version number. -u, --usage Show pointlessly short usage string. -d, --directory=DIR Use TrIPE directory DIR. -a, --admin-socket=FILE Select socket to connect to.""" exit(0) elif o in ('-v', '--version'): version(stdout) exit(0) elif o in ('-u', '--usage'): usage(stdout) exit(0) elif o in ('-d', '--directory'): tripedir = v elif o in ('-a', '--admin-socket'): tripesock = v else: raise "can't happen!" if len(args) > 0: usage(stderr) exit(1) OS.chdir(tripedir) mon = Monitor(tripesock) root = MonitorWindow(mon) HookClient().hook(root.closehook, exit) G.main() if __name__ == '__main__': main() #----- That's all, folks ----------------------------------------------------