chiark / gitweb /
0031d36fcee85b72aa226a79f747742c11af2352
[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 def cmd_userpeer(user):
134   """
135   userpeer USER: Report the peer name for the named user.
136   """
137   try:
138     peer = CDB.init(opts.cdb)['U' + user]
139   except KeyError:
140     raise T.TripeJobError('unknown-user', user)
141   T.svcinfo(peer)
142
143 ## Dictionary mapping challenges to waiting passive-connection coroutines.
144 chalmap = {}
145
146 def cmd_passive(*args):
147   """
148   passive [OPTIONS] USER: Await the arrival of the named USER.
149
150   Report a challenge; when (and if!) the server receives a greeting quoting
151   this challenge, add the corresponding peer to the server.
152   """
153   timeout = 30
154   op = T.OptParse(args, ['-timeout'])
155   for opt in op:
156     if opt == '-timeout':
157       timeout = T.timespec(op.arg())
158   user, = op.rest(1, 1)
159   try:
160     peer = CDB.init(opts.cdb)['U' + user]
161   except KeyError:
162     raise T.TripeJobError('unknown-user', user)
163   chal = S.getchal()
164   cr = T.Coroutine.getcurrent()
165   timer = M.SelTimer(time() + timeout, lambda: cr.switch(None))
166   try:
167     T.svcinfo(chal)
168     chalmap[chal] = cr
169     addr = cr.parent.switch()
170     if addr is None:
171       raise T.TripeJobError('connect-timeout')
172     addpeer(Peer(peer), addr)
173   finally:
174     del chalmap[chal]
175
176 def notify(_, code, *rest):
177   """
178   Watch for notifications.
179
180   In particular, if a GREETing appears quoting a challenge in the chalmap
181   then wake up the corresponding coroutine.
182   """
183   if code != 'GREET':
184     return
185   chal = rest[0]
186   addr = rest[1:]
187   if chal in chalmap:
188     chalmap[chal].switch(addr)
189
190 ###--------------------------------------------------------------------------
191 ### Start up.
192
193 def setup():
194   """
195   Service setup.
196
197   Register the notification-watcher, and add the automatic active peers.
198   """
199   S.handler['NOTE'] = notify
200   S.watch('+n')
201   if opts.startup:
202     cdb = CDB.init(opts.cdb)
203     try:
204       autos = cdb['%AUTO']
205     except KeyError:
206       autos = ''
207     for name in M.split(autos)[0]:
208       try:
209         peer = Peer(name, cdb)
210         addpeer(peer, M.split(peer.get('peer'), quotep = True)[0])
211       except T.TripeJobError, err:
212         S.warn('connect', 'auto-add-failed', name, *err.args)
213
214 def parse_options():
215   """
216   Parse the command-line options.
217
218   Automatically changes directory to the requested configdir, and turns on
219   debugging.  Returns the options object.
220   """
221   op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
222                     version = '%%prog %s' % VERSION)
223
224   op.add_option('-a', '--admin-socket',
225                 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
226                 help = 'Select socket to connect to [default %default]')
227   op.add_option('-d', '--directory',
228                 metavar = 'DIR', dest = 'dir', default = T.configdir,
229                 help = 'Select current diretory [default %default]')
230   op.add_option('-p', '--peerdb',
231                 metavar = 'FILE', dest = 'cdb', default = T.peerdb,
232                 help = 'Select peers database [default %default]')
233   op.add_option('--daemon', dest = 'daemon',
234                 default = False, action = 'store_true',
235                 help = 'Become a daemon after successful initialization')
236   op.add_option('--debug', dest = 'debug',
237                 default = False, action = 'store_true',
238                 help = 'Emit debugging trace information')
239   op.add_option('--startup', dest = 'startup',
240                 default = False, action = 'store_true',
241                 help = 'Being called as part of the server startup')
242
243   opts, args = op.parse_args()
244   if args: op.error('no arguments permitted')
245   OS.chdir(opts.dir)
246   T._debug = opts.debug
247   return opts
248
249 ## Service table, for running manually.
250 service_info = [('connect', VERSION, {
251   'passive': (1, None, '[OPTIONS] USER', cmd_passive),
252   'active': (1, 1, 'PEER', cmd_active),
253   'info': (1, 1, 'PEER', cmd_info),
254   'list': (0, 0, '', cmd_list),
255   'userpeer': (1, 1, 'USER', cmd_userpeer)
256 })]
257
258 if __name__ == '__main__':
259   opts = parse_options()
260   T.runservices(opts.tripesock, service_info,
261                 setup = setup,
262                 daemon = opts.daemon)
263
264 ###----- That's all, folks --------------------------------------------------