svc/watch.in: Check that the crypto is working in adopted peers.
[tripe] / svc / connect.in
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 --------------------------------------------------