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