move sigint
[hippotat] / server
CommitLineData
094ee3a2 1#!/usr/bin/python3
3fba9787 2
37ab4cdc 3from hippotat import *
aa663282 4
e2d41dc1
IJ
5import sys
6import os
7
e2d41dc1
IJ
8import twisted.internet
9import twisted.internet.endpoints
e2d41dc1
IJ
10from twisted.web.server import NOT_DONE_YET
11from twisted.logger import LogLevel
12
5da7763e
IJ
13#import twisted.web.server import Site
14#from twisted.web.resource import Resource
3fba9787 15
e75e9c17
IJ
16from optparse import OptionParser
17from configparser import ConfigParser
18from configparser import NoOptionError
3fba9787 19
0ac316c8
IJ
20import collections
21
c4b6d990
IJ
22import syslog
23
b0cfbfce 24clients = { }
3fba9787 25
e75e9c17 26defcfg = '''
094ee3a2
IJ
27[DEFAULT]
28max_batch_down = 65536
29max_queue_time = 10
30max_request_time = 54
650a3251 31target_requests_outstanding = 3
094ee3a2 32
e75e9c17
IJ
33[virtual]
34mtu = 1500
35# network
36# [host]
37# [relay]
38
39[server]
e2d41dc1 40ipif = userv root ipif %(host)s,%(relay)s,%(mtu)s,slip %(network)s
5da7763e 41addrs = 127.0.0.1 ::1
aa663282 42port = 8099
e75e9c17 43
094ee3a2
IJ
44[limits]
45max_batch_down = 262144
46max_queue_time = 121
47max_request_time = 121
650a3251 48target_requests_outstanding = 10
ec88b1f1
IJ
49'''
50
aa663282
IJ
51#---------- error handling ----------
52
53def crash(err):
54 print('CRASH ', err, file=sys.stderr)
55 try: reactor.stop()
56 except twisted.internet.error.ReactorNotRunning: pass
57
58def crash_on_defer(defer):
59 defer.addErrback(lambda err: crash(err))
60
61def crash_on_critical(event):
62 if event.get('log_level') >= LogLevel.critical:
63 crash(twisted.logger.formatEvent(event))
64
5da7763e
IJ
65#---------- "router" ----------
66
ec0c4d95
IJ
67def route(packet, saddr, daddr):
68 print('TRACE ', saddr, daddr, packet)
5da7763e
IJ
69 try: client = clients[daddr]
70 except KeyError: dclient = None
71 if dclient is not None:
72 dclient.queue_outbound(packet)
3a6076b4 73 elif saddr.is_link_local or daddr.is_link_local:
ec0c4d95 74 log_discard(packet, saddr, daddr, 'link-local')
e2d41dc1 75 elif daddr == host or daddr not in network:
ec0c4d95 76 print('TRACE INBOUND ', saddr, daddr, packet)
5da7763e 77 queue_inbound(packet)
e2d41dc1 78 elif daddr == relay:
5da7763e
IJ
79 log_discard(packet, saddr, daddr, 'relay')
80 else:
81 log_discard(packet, saddr, daddr, 'no client')
82
83def log_discard(packet, saddr, daddr, why):
3a6076b4 84 print('DROP ', saddr, daddr, why)
ec0c4d95
IJ
85# syslog.syslog(syslog.LOG_DEBUG,
86# 'discarded packet %s -> %s (%s)' % (saddr, daddr, why))
5da7763e 87
5da7763e 88#---------- client ----------
c4b6d990 89
ec88b1f1 90class Client():
c4b6d990 91 def __init__(self, ip, cs):
ec88b1f1
IJ
92 # instance data members
93 self._ip = ip
94 self._cs = cs
95 self.pw = cfg.get(cs, 'password')
0ac316c8 96 self._rq = collections.deque() # requests
650a3251 97 # self._pq = PacketQueue(...)
c4b6d990
IJ
98 # plus from config:
99 # .max_batch_down
100 # .max_queue_time
101 # .max_request_time
650a3251
IJ
102 # .target_requests_outstanding
103 for k in ('max_batch_down','max_queue_time','max_request_time',
104 'target_requests_outstanding'):
ec88b1f1 105 req = cfg.getint(cs, k)
094ee3a2 106 limit = cfg.getint('limits',k)
c4b6d990 107 self.__dict__[k] = min(req, limit)
650a3251 108 self._pq = PacketQueue(self.max_queue_time)
c4b6d990
IJ
109
110 def process_arriving_data(self, d):
b0cfbfce 111 for packet in slip.decode(d):
5bae5ba3 112 (saddr, daddr) = packet_addrs(packet)
c4b6d990
IJ
113 if saddr != self._ip:
114 raise ValueError('wrong source address %s' % saddr)
ec0c4d95 115 route(packet, saddr, daddr)
ec88b1f1 116
c4b6d990
IJ
117 def _req_cancel(self, request):
118 request.finish()
119
120 def _req_error(self, err, request):
121 self._req_cancel(request)
122
0ac316c8 123 def queue_outbound(self, packet):
650a3251 124 self._pq.append(packet)
0ac316c8 125
c4b6d990
IJ
126 def http_request(self, request):
127 request.setHeader('Content-Type','application/octet-stream')
128 reactor.callLater(self.max_request_time, self._req_cancel, request)
129 request.notifyFinish().addErrback(self._req_error, request)
0ac316c8
IJ
130 self._rq.append(request)
131 self._check_outbound()
132
133 def _check_outbound(self):
134 while True:
135 try: request = self._rq[0]
136 except IndexError: request = None
137 if request and request.finished:
138 self._rq.popleft()
139 continue
140
650a3251 141 if not self._pq.nonempty():
0ac316c8
IJ
142 # no packets, oh well
143 continue
144
094ee3a2
IJ
145 if request is None:
146 # no request
147 break
148
149 # request, and also some non-expired packets
150 while True:
650a3251
IJ
151 packet = self.pq.popleft()
152 if packet is None: break
094ee3a2 153
b0cfbfce 154 encoded = slip.encode(packet)
094ee3a2
IJ
155
156 if request.sentLength > 0:
b0cfbfce 157 if (request.sentLength + len(slip.delimiter)
094ee3a2
IJ
158 + len(encoded) > self.max_batch_down):
159 break
b0cfbfce 160 request.write(slip.delimiter)
094ee3a2
IJ
161
162 request.write(encoded)
163 self._pq.popLeft()
164
165 assert(request.sentLength)
166 self._rq.popLeft()
167 request.finish()
168 # round again, looking for more to do
ec88b1f1 169
650a3251
IJ
170 while len(self._rq) > self.target_requests_outstanding:
171 request = self._rq.popleft()
172 request.finish()
173
5da7763e 174class IphttpResource(twisted.web.resource.Resource):
c1e4910b 175 isLeaf = True
5da7763e
IJ
176 def render_POST(self, request):
177 # find client, update config, etc.
e2d41dc1 178 ci = ipaddr(request.args['i'])
5da7763e
IJ
179 c = clients[ci]
180 pw = request.args['pw']
181 if pw != c.pw: raise ValueError('bad password')
182
183 # update config
184 for r, w in (('mbd', 'max_batch_down'),
185 ('mqt', 'max_queue_time'),
650a3251
IJ
186 ('mrt', 'max_request_time'),
187 ('tro', 'target_requests_outstanding')):
5da7763e
IJ
188 try: v = request.args[r]
189 except KeyError: continue
190 v = int(v)
191 c.__dict__[w] = v
192
193 try: d = request.args['d']
194 except KeyError: d = ''
195
196 c.process_arriving_data(d)
197 c.new_request(request)
198
8e279651 199 def render_GET(self, request):
040ff511 200 return b'<html><body>hippotat</body></html>'
8e279651 201
5da7763e
IJ
202def start_http():
203 resource = IphttpResource()
b11c6e7a 204 site = twisted.web.server.Site(resource)
e2d41dc1 205 for addrspec in cfg.get('server','addrs').split():
5da7763e
IJ
206 try:
207 addr = ipaddress.IPv4Address(addrspec)
208 endpointfactory = twisted.internet.endpoints.TCP4ServerEndpoint
209 except AddressValueError:
210 addr = ipaddress.IPv6Address(addrspec)
211 endpointfactory = twisted.internet.endpoints.TCP6ServerEndpoint
212 ep = endpointfactory(reactor, cfg.getint('server','port'), addr)
b11c6e7a 213 crash_on_defer(ep.listen(site))
5da7763e
IJ
214
215#---------- config and setup ----------
216
3fba9787
IJ
217def process_cfg():
218 global network
e75e9c17
IJ
219 global host
220 global relay
5bae5ba3 221 global ipif_command
3fba9787 222
ec88b1f1 223 network = ipnetwork(cfg.get('virtual','network'))
e75e9c17
IJ
224 if network.num_addresses < 3 + 2:
225 raise ValueError('network needs at least 2^3 addresses')
226
3fba9787 227 try:
e75e9c17
IJ
228 host = cfg.get('virtual','host')
229 except NoOptionError:
e2d41dc1 230 host = next(network.hosts())
e75e9c17
IJ
231
232 try:
233 relay = cfg.get('virtual','relay')
e2d41dc1 234 except NoOptionError:
e75e9c17 235 for search in network.hosts():
e2d41dc1 236 if search == host: continue
e75e9c17
IJ
237 relay = search
238 break
3fba9787 239
ec88b1f1
IJ
240 for cs in cfg.sections():
241 if not (':' in cs or '.' in cs): continue
e2d41dc1 242 ci = ipaddr(cs)
ec88b1f1
IJ
243 if ci not in network:
244 raise ValueError('client %s not in network' % ci)
245 if ci in clients:
246 raise ValueError('multiple client cfg sections for %s' % ci)
247 clients[ci] = Client(ci, cs)
3fba9787 248
e2d41dc1
IJ
249 global mtu
250 mtu = cfg.get('virtual','mtu')
251
5bae5ba3
IJ
252 iic_vars = { }
253 for k in ('host','relay','mtu','network'):
254 iic_vars[k] = globals()[k]
255
256 ipif_command = cfg.get('server','ipif', vars=iic_vars)
257
e75e9c17 258def startup():
e2d41dc1
IJ
259 global cfg
260
e75e9c17
IJ
261 op = OptionParser()
262 op.add_option('-c', '--config', dest='configfile',
263 default='/etc/hippottd/server.conf')
264 global opts
265 (opts, args) = op.parse_args()
266 if len(args): op.error('no non-option arguments please')
267
e2d41dc1
IJ
268 twisted.logger.globalLogPublisher.addObserver(crash_on_critical)
269
e75e9c17 270 cfg = ConfigParser()
5bae5ba3 271 cfg.read_string(defcfg)
e2d41dc1 272 cfg.read(opts.configfile)
5bae5ba3
IJ
273 process_cfg()
274
040ff511 275 start_ipif(ipif_command, route)
5bae5ba3 276 start_http()
e2d41dc1
IJ
277
278startup()
279reactor.run()
aa663282 280print('CRASHED (end)', file=sys.stderr)