chiark / gitweb /
dae116208dc105fb4875ab4a59ef9893afa587ed
[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 --------------------------------------------------