chiark / gitweb /
5f8940e34ddc5b527e50275fe2bd85b243203940
[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           priv = peer.get('priv', None),
96           mobile = peer.get('mobile', 'nil') in booltrue,
97           cork = peer.get('cork', 'nil') in booltrue,
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 --------------------------------------------------