| 1 | #! @PYTHON@ |
| 2 | ### -*-python-*- |
| 3 | ### |
| 4 | ### Service for establishing dynamic connections |
| 5 | ### |
| 6 | ### (c) 2006 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 | VERSION = '@VERSION@' |
| 28 | |
| 29 | ###-------------------------------------------------------------------------- |
| 30 | ### External dependencies. |
| 31 | |
| 32 | from optparse import OptionParser |
| 33 | import tripe as T |
| 34 | import os as OS |
| 35 | import cdb as CDB |
| 36 | import mLib as M |
| 37 | from time import time |
| 38 | |
| 39 | S = T.svcmgr |
| 40 | |
| 41 | ###-------------------------------------------------------------------------- |
| 42 | ### Main service machinery. |
| 43 | |
| 44 | _magic = ['_magic'] # An object distinct from all others |
| 45 | |
| 46 | class Peer (object): |
| 47 | """Representation of a peer in the database.""" |
| 48 | |
| 49 | def __init__(me, peer, cdb = None): |
| 50 | """ |
| 51 | Create a new peer, named PEER. |
| 52 | |
| 53 | Information about the peer is read from the database CDB, or the default |
| 54 | one given on the command-line. |
| 55 | """ |
| 56 | me.name = peer |
| 57 | try: |
| 58 | record = (cdb or CDB.init(opts.cdb))['P' + peer] |
| 59 | except KeyError: |
| 60 | raise T.TripeJobError('unknown-peer', peer) |
| 61 | me.__dict__.update(M.URLDecode(record, semip = True)) |
| 62 | |
| 63 | def get(me, key, default = _magic): |
| 64 | """ |
| 65 | Get the information stashed under KEY from the peer's database record. |
| 66 | |
| 67 | If DEFAULT is given, then use it if the database doesn't contain the |
| 68 | necessary information. If no DEFAULT is given, then report an error. |
| 69 | """ |
| 70 | attr = me.__dict__.get(key, default) |
| 71 | if attr is _magic: |
| 72 | raise T.TripeJobError('malformed-peer', me.name, 'missing-key', key) |
| 73 | return attr |
| 74 | |
| 75 | def list(me): |
| 76 | """ |
| 77 | Iterate over the available keys in the peer's database record. |
| 78 | """ |
| 79 | return me.__dict__.iterkeys() |
| 80 | |
| 81 | def addpeer(peer, addr): |
| 82 | """ |
| 83 | Process a connect request from a new peer PEER on address ADDR. |
| 84 | |
| 85 | Any existing peer with this name is disconnected from the server. |
| 86 | """ |
| 87 | if peer.name in S.list(): |
| 88 | S.kill(peer.name) |
| 89 | try: |
| 90 | S.add(peer.name, |
| 91 | tunnel = peer.get('tunnel', None), |
| 92 | keepalive = peer.get('keepalive', None), |
| 93 | key = peer.get('key', None), |
| 94 | cork = peer.get('cork', 'nil') in ['t', 'true', 'y', 'yes', 'on'], |
| 95 | *addr) |
| 96 | except T.TripeError, exc: |
| 97 | raise T.TripeJobError(*exc.args) |
| 98 | |
| 99 | def cmd_active(name): |
| 100 | """ |
| 101 | active NAME: Handle an active connection request for the peer called NAME. |
| 102 | |
| 103 | The appropriate address is read from the database automatically. |
| 104 | """ |
| 105 | peer = Peer(name) |
| 106 | addr = peer.get('peer') |
| 107 | if addr == 'PASSIVE': |
| 108 | raise T.TripeJobError('passive-peer', name) |
| 109 | addpeer(peer, M.split(addr, quotep = True)[0]) |
| 110 | |
| 111 | def cmd_list(): |
| 112 | """ |
| 113 | list: Report a list of the available active peers. |
| 114 | """ |
| 115 | cdb = CDB.init(opts.cdb) |
| 116 | for key in cdb.keys(): |
| 117 | if key.startswith('P') and Peer(key[1:]).get('peer', '') != 'PASSIVE': |
| 118 | T.svcinfo(key[1:]) |
| 119 | |
| 120 | def cmd_info(name): |
| 121 | """ |
| 122 | info NAME: Report the database entries for the named peer. |
| 123 | """ |
| 124 | peer = Peer(name) |
| 125 | items = list(peer.list()) |
| 126 | items.sort() |
| 127 | for i in items: |
| 128 | T.svcinfo('%s=%s' % (i, peer.get(i))) |
| 129 | |
| 130 | ## Dictionary mapping challenges to waiting passive-connection coroutines. |
| 131 | chalmap = {} |
| 132 | |
| 133 | def cmd_passive(*args): |
| 134 | """ |
| 135 | passive [OPTIONS] USER: Await the arrival of the named USER. |
| 136 | |
| 137 | Report a challenge; when (and if!) the server receives a greeting quoting |
| 138 | this challenge, add the corresponding peer to the server. |
| 139 | """ |
| 140 | timeout = 30 |
| 141 | op = T.OptParse(args, ['-timeout']) |
| 142 | for opt in op: |
| 143 | if opt == '-timeout': |
| 144 | timeout = T.timespec(op.arg()) |
| 145 | user, = op.rest(1, 1) |
| 146 | try: |
| 147 | peer = CDB.init(opts.cdb)['U' + user] |
| 148 | except KeyError: |
| 149 | raise T.TripeJobError('unknown-user', user) |
| 150 | chal = S.getchal() |
| 151 | cr = T.Coroutine.getcurrent() |
| 152 | timer = M.SelTimer(time() + timeout, lambda: cr.switch(None)) |
| 153 | try: |
| 154 | T.svcinfo(chal) |
| 155 | chalmap[chal] = cr |
| 156 | addr = cr.parent.switch() |
| 157 | if addr is None: |
| 158 | raise T.TripeJobError('connect-timeout') |
| 159 | addpeer(Peer(peer), addr) |
| 160 | finally: |
| 161 | del chalmap[chal] |
| 162 | |
| 163 | def notify(_, code, *rest): |
| 164 | """ |
| 165 | Watch for notifications. |
| 166 | |
| 167 | In particular, if a GREETing appears quoting a challenge in the chalmap |
| 168 | then wake up the corresponding coroutine. |
| 169 | """ |
| 170 | if code != 'GREET': |
| 171 | return |
| 172 | chal = rest[0] |
| 173 | addr = rest[1:] |
| 174 | if chal in chalmap: |
| 175 | chalmap[chal].switch(addr) |
| 176 | |
| 177 | ###-------------------------------------------------------------------------- |
| 178 | ### Start up. |
| 179 | |
| 180 | def setup(): |
| 181 | """ |
| 182 | Service setup. |
| 183 | |
| 184 | Register the notification-watcher, and add the automatic active peers. |
| 185 | """ |
| 186 | S.handler['NOTE'] = notify |
| 187 | S.watch('+n') |
| 188 | if opts.startup: |
| 189 | cdb = CDB.init(opts.cdb) |
| 190 | try: |
| 191 | autos = cdb['%AUTO'] |
| 192 | except KeyError: |
| 193 | autos = '' |
| 194 | for name in M.split(autos)[0]: |
| 195 | try: |
| 196 | peer = Peer(name, cdb) |
| 197 | addpeer(peer, M.split(peer.get('peer'), quotep = True)[0]) |
| 198 | except T.TripeJobError, err: |
| 199 | S.warn('connect', 'auto-add-failed', name, *err.args) |
| 200 | |
| 201 | def parse_options(): |
| 202 | """ |
| 203 | Parse the command-line options. |
| 204 | |
| 205 | Automatically changes directory to the requested configdir, and turns on |
| 206 | debugging. Returns the options object. |
| 207 | """ |
| 208 | op = OptionParser(usage = '%prog [-a FILE] [-d DIR]', |
| 209 | version = '%%prog %s' % VERSION) |
| 210 | |
| 211 | op.add_option('-a', '--admin-socket', |
| 212 | metavar = 'FILE', dest = 'tripesock', default = T.tripesock, |
| 213 | help = 'Select socket to connect to [default %default]') |
| 214 | op.add_option('-d', '--directory', |
| 215 | metavar = 'DIR', dest = 'dir', default = T.configdir, |
| 216 | help = 'Select current diretory [default %default]') |
| 217 | op.add_option('-p', '--peerdb', |
| 218 | metavar = 'FILE', dest = 'cdb', default = T.peerdb, |
| 219 | help = 'Select peers database [default %default]') |
| 220 | op.add_option('--daemon', dest = 'daemon', |
| 221 | default = False, action = 'store_true', |
| 222 | help = 'Become a daemon after successful initialization') |
| 223 | op.add_option('--debug', dest = 'debug', |
| 224 | default = False, action = 'store_true', |
| 225 | help = 'Emit debugging trace information') |
| 226 | op.add_option('--startup', dest = 'startup', |
| 227 | default = False, action = 'store_true', |
| 228 | help = 'Being called as part of the server startup') |
| 229 | |
| 230 | opts, args = op.parse_args() |
| 231 | if args: op.error('no arguments permitted') |
| 232 | OS.chdir(opts.dir) |
| 233 | T._debug = opts.debug |
| 234 | return opts |
| 235 | |
| 236 | ## Service table, for running manually. |
| 237 | service_info = [('connect', VERSION, { |
| 238 | 'passive': (1, None, '[OPTIONS] USER', cmd_passive), |
| 239 | 'active': (1, 1, 'PEER', cmd_active), |
| 240 | 'info': (1, 1, 'PEER', cmd_info), |
| 241 | 'list': (0, 0, '', cmd_list) |
| 242 | })] |
| 243 | |
| 244 | if __name__ == '__main__': |
| 245 | opts = parse_options() |
| 246 | T.runservices(opts.tripesock, service_info, |
| 247 | setup = setup, |
| 248 | daemon = opts.daemon) |
| 249 | |
| 250 | ###----- That's all, folks -------------------------------------------------- |