chiark / gitweb /
server/tests.at: Fix TRIPECTL_INTERACT argument order.
[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           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 --------------------------------------------------