Commit | Line | Data |
---|---|---|
a62f8e8a MW |
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 | cork = peer.get('cork', 'nil') in ['t', 'true', 'y', 'yes', 'on'], | |
94 | *addr) | |
95 | except T.TripeError, exc: | |
96 | raise T.TripeJobError(*exc.args) | |
97 | ||
98 | def cmd_active(name): | |
99 | """ | |
100 | active NAME: Handle an active connection request for the peer called NAME. | |
101 | ||
102 | The appropriate address is read from the database automatically. | |
103 | """ | |
104 | peer = Peer(name) | |
105 | addr = peer.get('peer') | |
106 | if addr == 'PASSIVE': | |
107 | raise T.TripeJobError('passive-peer', name) | |
108 | addpeer(peer, M.split(addr, quotep = True)[0]) | |
109 | ||
110 | def cmd_list(): | |
111 | """ | |
112 | list: Report a list of the available active peers. | |
113 | """ | |
114 | cdb = CDB.init(opts.cdb) | |
115 | for key in cdb.keys(): | |
116 | if key.startswith('P') and Peer(key[1:]).get('peer', '') != 'PASSIVE': | |
117 | T.svcinfo(key[1:]) | |
118 | ||
119 | def cmd_info(name): | |
120 | """ | |
121 | info NAME: Report the database entries for the named peer. | |
122 | """ | |
123 | peer = Peer(name) | |
124 | items = list(peer.list()) | |
125 | items.sort() | |
126 | for i in items: | |
127 | T.svcinfo('%s=%s' % (i, peer.get(i))) | |
128 | ||
129 | ## Dictionary mapping challenges to waiting passive-connection coroutines. | |
130 | chalmap = {} | |
131 | ||
132 | def cmd_passive(*args): | |
133 | """ | |
134 | passive [OPTIONS] USER: Await the arrival of the named USER. | |
135 | ||
136 | Report a challenge; when (and if!) the server receives a greeting quoting | |
137 | this challenge, add the corresponding peer to the server. | |
138 | """ | |
139 | timeout = 30 | |
140 | op = T.OptParse(args, ['-timeout']) | |
141 | for opt in op: | |
142 | if opt == '-timeout': | |
143 | timeout = T.timespec(op.arg()) | |
144 | user, = op.rest(1, 1) | |
145 | try: | |
146 | peer = CDB.init(opts.cdb)['U' + user] | |
147 | except KeyError: | |
148 | raise T.TripeJobError('unknown-user', user) | |
149 | chal = S.getchal() | |
150 | cr = T.Coroutine.getcurrent() | |
151 | timer = M.SelTimer(time() + timeout, lambda: cr.switch(None)) | |
152 | try: | |
153 | T.svcinfo(chal) | |
154 | chalmap[chal] = cr | |
155 | addr = cr.parent.switch() | |
156 | if addr is None: | |
157 | raise T.TripeJobError('connect-timeout') | |
158 | addpeer(Peer(peer), addr) | |
159 | finally: | |
160 | del chalmap[chal] | |
161 | ||
162 | def notify(_, code, *rest): | |
163 | """ | |
164 | Watch for notifications. | |
165 | ||
166 | In particular, if a GREETing appears quoting a challenge in the chalmap | |
167 | then wake up the corresponding coroutine. | |
168 | """ | |
169 | if code != 'GREET': | |
170 | return | |
171 | chal = rest[0] | |
172 | addr = rest[1:] | |
173 | if chal in chalmap: | |
174 | chalmap[chal].switch(addr) | |
175 | ||
176 | ###-------------------------------------------------------------------------- | |
177 | ### Start up. | |
178 | ||
179 | def setup(): | |
180 | """ | |
181 | Service setup. | |
182 | ||
183 | Register the notification-watcher, and add the automatic active peers. | |
184 | """ | |
185 | S.handler['NOTE'] = notify | |
186 | S.watch('+n') | |
187 | if opts.startup: | |
188 | cdb = CDB.init(opts.cdb) | |
189 | try: | |
190 | autos = cdb['%AUTO'] | |
191 | except KeyError: | |
192 | autos = '' | |
193 | for name in M.split(autos)[0]: | |
194 | try: | |
195 | peer = Peer(name, cdb) | |
196 | addpeer(peer, M.split(peer.get('peer'), quotep = True)[0]) | |
197 | except T.TripeJobError, err: | |
198 | S.warn('connect', 'auto-add-failed', name, *err.args) | |
199 | ||
200 | def parse_options(): | |
201 | """ | |
202 | Parse the command-line options. | |
203 | ||
204 | Automatically changes directory to the requested configdir, and turns on | |
205 | debugging. Returns the options object. | |
206 | """ | |
207 | op = OptionParser(usage = '%prog [-a FILE] [-d DIR]', | |
208 | version = '%%prog %s' % VERSION) | |
209 | ||
210 | op.add_option('-a', '--admin-socket', | |
211 | metavar = 'FILE', dest = 'tripesock', default = T.tripesock, | |
212 | help = 'Select socket to connect to [default %default]') | |
213 | op.add_option('-d', '--directory', | |
214 | metavar = 'DIR', dest = 'dir', default = T.configdir, | |
215 | help = 'Select current diretory [default %default]') | |
216 | op.add_option('-p', '--peerdb', | |
217 | metavar = 'FILE', dest = 'cdb', default = T.peerdb, | |
218 | help = 'Select peers database [default %default]') | |
219 | op.add_option('--daemon', dest = 'daemon', | |
220 | default = False, action = 'store_true', | |
221 | help = 'Become a daemon after successful initialization') | |
222 | op.add_option('--debug', dest = 'debug', | |
223 | default = False, action = 'store_true', | |
224 | help = 'Emit debugging trace information') | |
225 | op.add_option('--startup', dest = 'startup', | |
226 | default = False, action = 'store_true', | |
227 | help = 'Being called as part of the server startup') | |
228 | ||
229 | opts, args = op.parse_args() | |
230 | if args: op.error('no arguments permitted') | |
231 | OS.chdir(opts.dir) | |
232 | T._debug = opts.debug | |
233 | return opts | |
234 | ||
235 | ## Service table, for running manually. | |
236 | service_info = [('connect', VERSION, { | |
237 | 'passive': (1, None, '[OPTIONS] USER', cmd_passive), | |
238 | 'active': (1, 1, 'PEER', cmd_active), | |
239 | 'info': (1, 1, 'PEER', cmd_info), | |
240 | 'list': (0, 0, '', cmd_list) | |
241 | })] | |
242 | ||
243 | if __name__ == '__main__': | |
244 | opts = parse_options() | |
245 | T.runservices(opts.tripesock, service_info, | |
246 | setup = setup, | |
247 | daemon = opts.daemon) | |
248 | ||
249 | ###----- That's all, folks -------------------------------------------------- |