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