ensure mtu is in the ipif substitution set
[hippotat] / hippotatlib / __init__.py
CommitLineData
b0cfbfce 1# -*- python -*-
0256fc10
IJ
2#
3# Hippotat - Asinine IP Over HTTP program
4# hippotatlib/__init__.py - common library code
5#
6# Copyright 2017 Ian Jackson
7#
f85d143f 8# GPLv3+
0256fc10 9#
f85d143f
IJ
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
0256fc10 14#
f85d143f
IJ
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program, in the file GPLv3. If not,
22# see <http://www.gnu.org/licenses/>.
23
b0cfbfce 24
37ab4cdc
IJ
25import signal
26signal.signal(signal.SIGINT, signal.SIG_DFL)
27
1321ad5f 28import sys
cae50358 29import os
1321ad5f 30
b83d422a
IJ
31from zope.interface import implementer
32
040ff511
IJ
33import twisted
34from twisted.internet import reactor
1d023c89 35import twisted.internet.endpoints
8c3b6620
IJ
36import twisted.logger
37from twisted.logger import LogLevel
38import twisted.python.constants
39from twisted.python.constants import NamedConstant
b0cfbfce
IJ
40
41import ipaddress
42from ipaddress import AddressValueError
43
ae7c7784 44from optparse import OptionParser
5510890e 45import configparser
ae7c7784
IJ
46from configparser import ConfigParser
47from configparser import NoOptionError
48
c13ee6e6
IJ
49from functools import partial
50
ae7c7784 51import collections
84e763c7 52import time
ef041033
IJ
53import hmac
54import hashlib
55import base64
8c3b6620 56import codecs
eedc8b30 57import traceback
ae7c7784 58
1321ad5f
IJ
59import re as regexp
60
5a37bac8 61import hippotatlib.slip as slip
1321ad5f 62
d579a048 63class DBG(twisted.python.constants.Names):
380ed56c 64 INIT = NamedConstant()
cae50358 65 CONFIG = NamedConstant()
d579a048 66 ROUTE = NamedConstant()
b68c0739 67 DROP = NamedConstant()
4a780703 68 OWNSOURCE = NamedConstant()
d579a048
IJ
69 FLOW = NamedConstant()
70 HTTP = NamedConstant()
380ed56c 71 TWISTED = NamedConstant()
d579a048 72 QUEUE = NamedConstant()
380ed56c 73 HTTP_CTRL = NamedConstant()
d579a048 74 QUEUE_CTRL = NamedConstant()
297b3ebf 75 HTTP_FULL = NamedConstant()
0accf0d3 76 CTRL_DUMP = NamedConstant()
380ed56c 77 SLIP_FULL = NamedConstant()
9acb0eca 78 DATA_COMPLETE = NamedConstant()
d579a048 79
b68c0739 80_hex_codec = codecs.getencoder('hex_codec')
8c3b6620 81
b83d422a
IJ
82#---------- logging ----------
83
84org_stderr = sys.stderr
85
8c3b6620
IJ
86log = twisted.logger.Logger()
87
2e68eb10
IJ
88debug_set = set()
89debug_def_detail = DBG.HTTP
3e35fc99 90
8c3b6620 91def log_debug(dflag, msg, idof=None, d=None):
3e35fc99 92 if dflag not in debug_set: return
e8fcf3b7 93 #print('---------------->',repr((dflag, msg, idof, d)), file=sys.stderr)
8c3b6620 94 if idof is not None:
e8ed0029 95 msg = '[%#x] %s' % (id(idof), msg)
8c3b6620 96 if d is not None:
9acb0eca
IJ
97 trunc = ''
98 if not DBG.DATA_COMPLETE in debug_set:
99 if len(d) > 64:
100 d = d[0:64]
101 trunc = '...'
b68c0739 102 d = _hex_codec(d)[0].decode('ascii')
9acb0eca 103 msg += ' ' + d + trunc
8c3b6620
IJ
104 log.info('{dflag} {msgcore}', dflag=dflag, msgcore=msg)
105
80e963a1
IJ
106def logevent_is_boringtwisted(event):
107 try:
108 if event.get('log_level') != LogLevel.info:
109 return False
110 dflag = event.get('dflag')
111 if dflag is False : return False
112 if dflag in debug_set: return False
113 if dflag is None and DBG.TWISTED in debug_set: return False
114 return True
115 except Exception:
02a201e1
IJ
116 print('EXCEPTION (IN BORINGTWISTED CHECK)',
117 traceback.format_exc(), file=org_stderr)
80e963a1
IJ
118 return False
119
b83d422a
IJ
120@implementer(twisted.logger.ILogFilterPredicate)
121class LogNotBoringTwisted:
122 def __call__(self, event):
80e963a1
IJ
123 return (
124 twisted.logger.PredicateResult.no
125 if logevent_is_boringtwisted(event) else
126 twisted.logger.PredicateResult.yes
127 )
b83d422a
IJ
128
129#---------- default config ----------
130
ca732796 131defcfg = '''
71f9ddb6 132[COMMON]
9e445690
IJ
133max_batch_down = 65536
134max_queue_time = 10
135target_requests_outstanding = 3
136http_timeout = 30
137http_timeout_grace = 5
138max_requests_outstanding = 6
139max_batch_up = 4000
140http_retry = 5
c7fb640e 141port = 80
8d374606 142vroutes = ''
d72f8360
IJ
143ifname_client = hippo%%d
144ifname_server = shippo%%d
ef041033 145max_clock_skew = 300
ca732796
IJ
146
147#[server] or [<client>] overrides
d72f8360 148ipif = userv root ipif %(local)s,%(peer)s,%(mtu)s,slip,%(ifname)s %(rnets)s
ca732796 149
9e445690 150# relating to virtual network
ca732796 151mtu = 1500
ca732796 152
9e445690 153# addrs = 127.0.0.1 ::1
9e445690
IJ
154# url
155
156# relating to virtual network
8d374606
IJ
157vvnetwork = 172.24.230.192
158# vnetwork = <prefix>/<len>
c57e18a7 159# vaddr = <ipaddr>
8d374606 160# vrelay = <ipaddr>
9e445690 161
ca732796
IJ
162
163# [<client-ip4-or-ipv6-address>]
dce21e00 164# secret = <secret> # used by both, must match
ca732796 165
c7fb640e 166[LIMIT]
9e445690
IJ
167max_batch_down = 262144
168max_queue_time = 121
169http_timeout = 121
170target_requests_outstanding = 10
ca732796
IJ
171'''
172
87a7c0c7 173# these need to be defined here so that they can be imported by import *
cae50358 174cfg = ConfigParser(strict=False)
ae7c7784
IJ
175optparser = OptionParser()
176
e4006ac4 177_mimetrans = bytes.maketrans(b'-'+slip.esc, slip.esc+b'-')
7b07f0b5
IJ
178def mime_translate(s):
179 # SLIP-encoded packets cannot contain ESC ESC.
180 # Swap `-' and ESC. The result cannot contain `--'
181 return s.translate(_mimetrans)
182
87a7c0c7 183class ConfigResults:
c7fb640e
IJ
184 def __init__(self):
185 pass
87a7c0c7
IJ
186 def __repr__(self):
187 return 'ConfigResults('+repr(self.__dict__)+')'
188
a8827d59 189def log_discard(packet, iface, saddr, daddr, why):
b68c0739 190 log_debug(DBG.DROP,
a8827d59 191 'discarded packet [%s] %s -> %s: %s' % (iface, saddr, daddr, why),
b68c0739 192 d=packet)
1321ad5f 193
b0cfbfce
IJ
194#---------- packet parsing ----------
195
196def packet_addrs(packet):
197 version = packet[0] >> 4
198 if version == 4:
199 addrlen = 4
200 saddroff = 3*4
201 factory = ipaddress.IPv4Address
202 elif version == 6:
203 addrlen = 16
204 saddroff = 2*4
205 factory = ipaddress.IPv6Address
206 else:
207 raise ValueError('unsupported IP version %d' % version)
208 saddr = factory(packet[ saddroff : saddroff + addrlen ])
209 daddr = factory(packet[ saddroff + addrlen : saddroff + addrlen*2 ])
210 return (saddr, daddr)
211
212#---------- address handling ----------
213
214def ipaddr(input):
215 try:
216 r = ipaddress.IPv4Address(input)
217 except AddressValueError:
218 r = ipaddress.IPv6Address(input)
219 return r
220
221def ipnetwork(input):
222 try:
223 r = ipaddress.IPv4Network(input)
224 except NetworkValueError:
225 r = ipaddress.IPv6Network(input)
226 return r
040ff511
IJ
227
228#---------- ipif (SLIP) subprocess ----------
229
a95cfeb2 230class SlipStreamDecoder():
db6ba584 231 def __init__(self, desc, on_packet):
040ff511 232 self._buffer = b''
a95cfeb2 233 self._on_packet = on_packet
db6ba584
IJ
234 self._desc = desc
235 self._log('__init__')
236
237 def _log(self, msg, **kwargs):
3297cac1 238 log_debug(DBG.SLIP_FULL, 'slip %s: %s' % (self._desc, msg), **kwargs)
a95cfeb2
IJ
239
240 def inputdata(self, data):
db6ba584 241 self._log('inputdata', d=data)
7fa9c132
IJ
242 data = self._buffer + data
243 self._buffer = b''
244 packets = slip.decode(data, True)
040ff511
IJ
245 self._buffer = packets.pop()
246 for packet in packets:
a95cfeb2 247 self._maybe_packet(packet)
54890d4d 248 self._log('bufremain', d=self._buffer)
a95cfeb2
IJ
249
250 def _maybe_packet(self, packet):
54890d4d 251 self._log('maybepacket', d=packet)
db6ba584
IJ
252 if len(packet):
253 self._on_packet(packet)
a95cfeb2 254
4f991c0c 255 def flush(self):
54890d4d 256 self._log('flush')
7fa9c132 257 data = self._buffer
a95cfeb2 258 self._buffer = b''
7fa9c132
IJ
259 packets = slip.decode(data)
260 assert(len(packets) == 1)
261 self._maybe_packet(packets[0])
4f991c0c 262
e4006ac4 263class _IpifProcessProtocol(twisted.internet.protocol.ProcessProtocol):
4f991c0c
IJ
264 def __init__(self, router):
265 self._router = router
db6ba584 266 self._decoder = SlipStreamDecoder('ipif', self.slip_on_packet)
a95cfeb2
IJ
267 def connectionMade(self): pass
268 def outReceived(self, data):
269 self._decoder.inputdata(data)
270 def slip_on_packet(self, packet):
4f991c0c
IJ
271 (saddr, daddr) = packet_addrs(packet)
272 if saddr.is_link_local or daddr.is_link_local:
a8827d59 273 log_discard(packet, 'ipif', saddr, daddr, 'link-local')
4f991c0c
IJ
274 return
275 self._router(packet, saddr, daddr)
040ff511
IJ
276 def processEnded(self, status):
277 status.raiseException()
278
279def start_ipif(command, router):
040ff511
IJ
280 ipif = _IpifProcessProtocol(router)
281 reactor.spawnProcess(ipif,
282 '/bin/sh',['sh','-xc', command],
ff613365
IJ
283 childFDs={0:'w', 1:'r', 2:2},
284 env=None)
909e0ff3 285 return ipif
040ff511 286
909e0ff3 287def queue_inbound(ipif, packet):
15407d80 288 log_debug(DBG.FLOW, "queue_inbound", d=packet)
040ff511
IJ
289 ipif.transport.write(slip.delimiter)
290 ipif.transport.write(slip.encode(packet))
291 ipif.transport.write(slip.delimiter)
292
650a3251
IJ
293#---------- packet queue ----------
294
295class PacketQueue():
d579a048
IJ
296 def __init__(self, desc, max_queue_time):
297 self._desc = desc
8718b02c 298 assert(desc + '')
650a3251
IJ
299 self._max_queue_time = max_queue_time
300 self._pq = collections.deque() # packets
301
b68c0739 302 def _log(self, dflag, msg, **kwargs):
8c3b6620 303 log_debug(dflag, self._desc+' pq: '+msg, **kwargs)
d579a048 304
650a3251 305 def append(self, packet):
8c3b6620 306 self._log(DBG.QUEUE, 'append', d=packet)
650a3251
IJ
307 self._pq.append((time.monotonic(), packet))
308
309 def nonempty(self):
8c3b6620 310 self._log(DBG.QUEUE, 'nonempty ?')
650a3251
IJ
311 while True:
312 try: (queuetime, packet) = self._pq[0]
8c3b6620
IJ
313 except IndexError:
314 self._log(DBG.QUEUE, 'nonempty ? empty.')
315 return False
650a3251
IJ
316
317 age = time.monotonic() - queuetime
84e763c7 318 if age > self._max_queue_time:
650a3251 319 # strip old packets off the front
8c3b6620 320 self._log(DBG.QUEUE, 'dropping (old)', d=packet)
650a3251
IJ
321 self._pq.popleft()
322 continue
323
8c3b6620 324 self._log(DBG.QUEUE, 'nonempty ? nonempty.')
650a3251
IJ
325 return True
326
7b07f0b5
IJ
327 def process(self, sizequery, moredata, max_batch):
328 # sizequery() should return size of batch so far
329 # moredata(s) should add s to batch
8c3b6620 330 self._log(DBG.QUEUE, 'process...')
7b07f0b5
IJ
331 while True:
332 try: (dummy, packet) = self._pq[0]
8c3b6620
IJ
333 except IndexError:
334 self._log(DBG.QUEUE, 'process... empty')
335 break
336
337 self._log(DBG.QUEUE_CTRL, 'process... packet', d=packet)
7b07f0b5
IJ
338
339 encoded = slip.encode(packet)
340 sofar = sizequery()
341
8c3b6620
IJ
342 self._log(DBG.QUEUE_CTRL,
343 'process... (sofar=%d, max=%d) encoded' % (sofar, max_batch),
b68c0739 344 d=encoded)
8c3b6620 345
7b07f0b5
IJ
346 if sofar > 0:
347 if sofar + len(slip.delimiter) + len(encoded) > max_batch:
8c3b6620 348 self._log(DBG.QUEUE_CTRL, 'process... overflow')
7b07f0b5
IJ
349 break
350 moredata(slip.delimiter)
351
352 moredata(encoded)
84e763c7 353 self._pq.popleft()
ae7c7784
IJ
354
355#---------- error handling ----------
356
b68c0739
IJ
357_crashing = False
358
ae7c7784 359def crash(err):
b68c0739
IJ
360 global _crashing
361 _crashing = True
e8ed0029
IJ
362 print('========== CRASH ==========', err,
363 '===========================', file=sys.stderr)
ae7c7784
IJ
364 try: reactor.stop()
365 except twisted.internet.error.ReactorNotRunning: pass
366
367def crash_on_defer(defer):
368 defer.addErrback(lambda err: crash(err))
369
e4006ac4 370def crash_on_critical(event):
ae7c7784
IJ
371 if event.get('log_level') >= LogLevel.critical:
372 crash(twisted.logger.formatEvent(event))
373
ef041033
IJ
374#---------- authentication tokens ----------
375
376_authtoken_digest = hashlib.sha256
377
378def _authtoken_time():
379 return int(time.time())
380
381def _authtoken_hmac(secret, hextime):
382 return hmac.new(secret, hextime, _authtoken_digest).digest()
383
384def authtoken_make(secret):
385 hextime = ('%x' % _authtoken_time()).encode('ascii')
386 mac = _authtoken_hmac(secret, hextime)
387 return hextime + b' ' + base64.b64encode(mac)
388
389def authtoken_check(secret, token, maxskew):
390 (hextime, theirmac64) = token.split(b' ')
391 now = _authtoken_time()
392 then = int(hextime, 16)
393 skew = then - now;
394 if (abs(skew) > maxskew):
395 raise ValueError('too much clock skew (client %ds ahead)' % skew)
396 theirmac = base64.b64decode(theirmac64)
397 ourmac = _authtoken_hmac(secret, hextime)
398 if not hmac.compare_digest(theirmac, ourmac):
399 raise ValueError('invalid token (wrong secret?)')
400 pass
401
87a7c0c7
IJ
402#---------- config processing ----------
403
c7fb640e
IJ
404def _cfg_process_putatives():
405 servers = { }
406 clients = { }
407 # maps from abstract object to canonical name for cs's
87a7c0c7 408
c7fb640e
IJ
409 def putative(cmap, abstract, canoncs):
410 try:
411 current_canoncs = cmap[abstract]
412 except KeyError:
413 pass
414 else:
415 assert(current_canoncs == canoncs)
416 cmap[abstract] = canoncs
417
418 server_pat = r'[-.0-9A-Za-z]+'
419 client_pat = r'[.:0-9a-f]+'
420 server_re = regexp.compile(server_pat)
6d5e8381
IJ
421 serverclient_re = regexp.compile(
422 server_pat + r' ' + '(?:' + client_pat + '|LIMIT)')
88487243 423
c7fb640e 424 for cs in cfg.sections():
3a8ed92d
IJ
425 def dbg(m):
426 log_debug_config('putatives: section [%s] %s' % (cs, m))
4652e382 427
43dd2ce0 428 def log_ignore(why):
3a8ed92d 429 dbg('X ignore: %s' % (why))
43dd2ce0
IJ
430 print('warning: ignoring config section [%s] (%s)' % (cs, why),
431 file=sys.stderr)
432
71f9ddb6
IJ
433 if cs == 'LIMIT' or cs == 'COMMON':
434 # plan A "[LIMIT]" or "[COMMON]"
3a8ed92d 435 dbg('A ignore')
c7fb640e 436 continue
88487243 437
c7fb640e
IJ
438 try:
439 # plan B "[<client>]" part 1
440 ci = ipaddr(cs)
441 except AddressValueError:
88487243 442
c7fb640e
IJ
443 if server_re.fullmatch(cs):
444 # plan C "[<servername>]"
3a8ed92d 445 dbg('C <server>')
c7fb640e
IJ
446 putative(servers, cs, cs)
447 continue
448
449 if serverclient_re.fullmatch(cs):
450 # plan D "[<servername> <client>]" part 1
451 (pss,pcs) = cs.split(' ')
452
8d374606 453 if pcs == 'LIMIT':
c7fb640e 454 # plan E "[<servername> LIMIT]"
3a8ed92d 455 dbg('E <server> LIMIT')
c7fb640e
IJ
456 continue
457
458 try:
459 # plan D "[<servername> <client>]" part 2
b8c38e66 460 ci = ipaddr(pcs)
c7fb640e 461 except AddressValueError:
343c8cf4 462 # plan F branch 1 "[<some thing we do not understand>]"
43dd2ce0 463 log_ignore('bad-addr')
c7fb640e
IJ
464 continue
465
466 else: # no AddressValueError
4652e382 467 # plan D "[<servername> <client>]" part 3
3a8ed92d 468 dbg('D <server> <client>')
c7fb640e
IJ
469 putative(clients, ci, pcs)
470 putative(servers, pss, pss)
471 continue
343c8cf4
IJ
472 else:
473 # plan F branch 2 "[<some thing we do not understand>]"
474 log_ignore('nomatch '+ repr(serverclient_re))
c7fb640e
IJ
475
476 else: # no AddressValueError
477 # plan B "[<client>" part 2
3a8ed92d 478 dbg('B <client>')
c7fb640e
IJ
479 putative(clients, ci, cs)
480 continue
481
482 return (servers, clients)
483
62d13acc 484def cfg_process_general(c, ss):
300fe4ed 485 c.mtu = cfg1getint(ss, 'mtu')
c7fb640e
IJ
486
487def cfg_process_saddrs(c, ss):
488 class ServerAddr():
489 def __init__(self, port, addrspec):
490 self.port = port
491 # also self.addr
492 try:
493 self.addr = ipaddress.IPv4Address(addrspec)
494 self._endpointfactory = twisted.internet.endpoints.TCP4ServerEndpoint
495 self._inurl = b'%s'
496 except AddressValueError:
497 self.addr = ipaddress.IPv6Address(addrspec)
498 self._endpointfactory = twisted.internet.endpoints.TCP6ServerEndpoint
499 self._inurl = b'[%s]'
500 def make_endpoint(self):
3b69fba9
IJ
501 return self._endpointfactory(reactor, self.port,
502 interface= '%s' % self.addr)
c7fb640e
IJ
503 def url(self):
504 url = b'http://' + (self._inurl % str(self.addr).encode('ascii'))
505 if self.port != 80: url += b':%d' % self.port
506 url += b'/'
507 return url
3b69fba9
IJ
508 def __repr__(self):
509 return 'ServerAddr'+repr((self.port,self.addr))
c7fb640e 510
300fe4ed 511 c.port = cfg1getint(ss,'port')
c7fb640e 512 c.saddrs = [ ]
300fe4ed 513 for addrspec in cfg1get(ss, 'addrs').split():
c7fb640e
IJ
514 sa = ServerAddr(c.port, addrspec)
515 c.saddrs.append(sa)
516
517def cfg_process_vnetwork(c, ss):
300fe4ed 518 c.vnetwork = ipnetwork(cfg1get(ss,'vnetwork'))
c7f134ce
IJ
519 if c.vnetwork.num_addresses < 3 + 2:
520 raise ValueError('vnetwork needs at least 2^3 addresses')
88487243 521
8d374606 522def cfg_process_vaddr(c, ss):
88487243 523 try:
300fe4ed 524 c.vaddr = cfg1get(ss,'vaddr')
88487243 525 except NoOptionError:
8d374606 526 cfg_process_vnetwork(c, ss)
c7f134ce 527 c.vaddr = next(c.vnetwork.hosts())
88487243 528
c7fb640e
IJ
529def cfg_search_section(key,sections):
530 for section in sections:
531 if cfg.has_option(section, key):
532 return section
8d374606 533 raise NoOptionError(key, repr(sections))
c7fb640e 534
fa63bd93
IJ
535def cfg_get_raw(*args, **kwargs):
536 # for passing to cfg_search
537 return cfg.get(*args, raw=True, **kwargs)
538
c7fb640e
IJ
539def cfg_search(getter,key,sections):
540 section = cfg_search_section(key,sections)
541 return getter(section, key)
542
71f9ddb6
IJ
543def cfg1get(section,key, getter=cfg.get,**kwargs):
544 section = cfg_search_section(key,[section,'COMMON'])
545 return getter(section,key,**kwargs)
300fe4ed 546
71f9ddb6
IJ
547def cfg1getint(section,key, **kwargs):
548 return cfg1get(section,key, getter=cfg.getint,**kwargs);
300fe4ed 549
c7fb640e 550def cfg_process_client_limited(cc,ss,sections,key):
300fe4ed
IJ
551 val = cfg_search(cfg1getint, key, sections)
552 lim = cfg_search(cfg1getint, key, ['%s LIMIT' % ss, 'LIMIT'])
c7fb640e
IJ
553 cc.__dict__[key] = min(val,lim)
554
555def cfg_process_client_common(cc,ss,cs,ci):
dce21e00 556 # returns sections to search in, iff secret is defined, otherwise None
c7fb640e
IJ
557 cc.ci = ci
558
8d374606 559 sections = ['%s %s' % (ss,cs),
c7fb640e
IJ
560 cs,
561 ss,
71f9ddb6 562 'COMMON']
c7fb640e 563
dce21e00 564 try: pwsection = cfg_search_section('secret', sections)
c7fb640e 565 except NoOptionError: return None
88487243 566
dce21e00
IJ
567 pw = cfg1get(pwsection, 'secret')
568 cc.secret = pw.encode('utf-8')
88487243 569
c7fb640e
IJ
570 cfg_process_client_limited(cc,ss,sections,'target_requests_outstanding')
571 cfg_process_client_limited(cc,ss,sections,'http_timeout')
88487243 572
c7fb640e
IJ
573 return sections
574
8d374606 575def cfg_process_ipif(c, sections, varmap):
c7fb640e
IJ
576 for d, s in varmap:
577 try: v = getattr(c, s)
578 except AttributeError: continue
579 setattr(c, d, v)
a14782d3
IJ
580 for d in ('mtu',):
581 v = cfg_search(cfg.get, d, sections)
582 setattr(c, d, v)
c7fb640e 583
c7f134ce 584 #print('CFGIPIF',repr((varmap, sections, c.__dict__)),file=sys.stderr)
8d374606 585
c7fb640e 586 section = cfg_search_section('ipif', sections)
300fe4ed 587 c.ipif_command = cfg1get(section,'ipif', vars=c.__dict__)
88487243 588
ae7c7784
IJ
589#---------- startup ----------
590
8c771381
IJ
591def log_debug_config(m):
592 if not DBG.CONFIG in debug_set: return
593 print('DBG.CONFIG:', m)
594
5510890e 595def common_startup(process_cfg):
c7fb640e
IJ
596 # calls process_cfg(putative_clients, putative_servers)
597
82302bac 598 # ConfigParser hates #-comments after values
c7fb640e 599 trailingcomments_re = regexp.compile(r'#.*')
82302bac 600 cfg.read_string(trailingcomments_re.sub('', defcfg))
cae50358
IJ
601 need_defcfg = True
602
603 def readconfig(pathname, mandatory=True):
604 def log(m, p=pathname):
605 if not DBG.CONFIG in debug_set: return
00ea5443 606 log_debug_config('%s: %s' % (m, p))
cae50358
IJ
607
608 try:
609 files = os.listdir(pathname)
610
611 except FileNotFoundError:
612 if mandatory: raise
613 log('skipped')
614 return
615
616 except NotADirectoryError:
617 cfg.read(pathname)
618 log('read file')
619 return
620
621 # is a directory
622 log('directory')
623 re = regexp.compile('[^-A-Za-z0-9_]')
2b13e1cc 624 for f in os.listdir(pathname):
cae50358
IJ
625 if re.search(f): continue
626 subpath = pathname + '/' + f
627 try:
628 os.stat(subpath)
629 except FileNotFoundError:
630 log('entry skipped', subpath)
631 continue
632 cfg.read(subpath)
633 log('entry read', subpath)
634
635 def oc_config(od,os, value, op):
636 nonlocal need_defcfg
637 need_defcfg = False
638 readconfig(value)
2e68eb10 639
26f04eff
IJ
640 def oc_extra_config(od,os, value, op):
641 readconfig(value)
642
7852bfc8
IJ
643 def read_defconfig():
644 readconfig('/etc/hippotat/config.d', False)
dce21e00 645 readconfig('/etc/hippotat/secrets.d', False)
7852bfc8
IJ
646 readconfig('/etc/hippotat/master.cfg', False)
647
26f04eff
IJ
648 def oc_defconfig(od,os, value, op):
649 nonlocal need_defcfg
650 need_defcfg = False
651 read_defconfig(value)
652
9acb0eca
IJ
653 def dfs_less_detailed(dl):
654 return [df for df in DBG.iterconstants() if df <= dl]
655
656 def ds_default(od,os,dl,op):
2e68eb10 657 global debug_set
ff0fc3fa
IJ
658 debug_set.clear
659 debug_set |= set(dfs_less_detailed(debug_def_detail))
2e68eb10 660
9acb0eca 661 def ds_select(od,os, spec, op):
9acb0eca
IJ
662 for it in spec.split(','):
663
9acb0eca
IJ
664 if it.startswith('-'):
665 mutator = debug_set.discard
666 it = it[1:]
667 else:
668 mutator = debug_set.add
2cf75145
IJ
669
670 if it == '+':
671 dfs = DBG.iterconstants()
672
673 else:
674 if it.endswith('+'):
675 mapper = dfs_less_detailed
676 it = it[0:len(it)-1]
677 else:
678 mapper = lambda x: [x]
679
680 try:
681 dfspec = DBG.lookupByName(it)
682 except ValueError:
683 optparser.error('unknown debug flag %s in --debug-select' % it)
684
685 dfs = mapper(dfspec)
686
687 for df in dfs:
688 mutator(df)
9acb0eca
IJ
689
690 optparser.add_option('-D', '--debug',
2e68eb10
IJ
691 nargs=0,
692 action='callback',
9acb0eca
IJ
693 help='enable default debug (to stdout)',
694 callback= ds_default)
695
696 optparser.add_option('--debug-select',
697 nargs=1,
698 type='string',
2cf75145 699 metavar='[-]DFLAG[+]|[-]+,...',
9acb0eca 700 help=
2cf75145
IJ
701'''enable (`-': disable) each specified DFLAG;
702`+': do same for all "more interesting" DFLAGSs;
703just `+': all DFLAGs.
704 DFLAGS: ''' + ' '.join([df.name for df in DBG.iterconstants()]),
9acb0eca
IJ
705 action='callback',
706 callback= ds_select)
2e68eb10 707
cae50358
IJ
708 optparser.add_option('-c', '--config',
709 nargs=1,
710 type='string',
711 metavar='CONFIGFILE',
712 dest='configfile',
713 action='callback',
714 callback= oc_config)
715
26f04eff
IJ
716 optparser.add_option('--extra-config',
717 nargs=1,
718 type='string',
719 metavar='CONFIGFILE',
720 dest='configfile',
721 action='callback',
722 callback= oc_extra_config)
723
724 optparser.add_option('--default-config',
725 action='callback',
726 callback= oc_defconfig)
727
f022d67f
IJ
728 (opts, args) = optparser.parse_args()
729 if len(args): optparser.error('no non-option arguments please')
2e68eb10 730
cae50358 731 if need_defcfg:
7852bfc8 732 read_defconfig()
9acb0eca 733
c7fb640e 734 try:
8d374606 735 (pss, pcs) = _cfg_process_putatives()
1cc6968f 736 process_cfg(opts, pss, pcs)
5510890e
IJ
737 except (configparser.Error, ValueError):
738 traceback.print_exc(file=sys.stderr)
739 print('\nInvalid configuration, giving up.', file=sys.stderr)
740 sys.exit(12)
741
ff0fc3fa
IJ
742
743 #print('X', debug_set, file=sys.stderr)
2e68eb10 744
8c3b6620 745 log_formatter = twisted.logger.formatEventAsClassicLogText
389236df
IJ
746 stdout_obs = twisted.logger.FileLogObserver(sys.stdout, log_formatter)
747 stderr_obs = twisted.logger.FileLogObserver(sys.stderr, log_formatter)
748 pred = twisted.logger.LogLevelFilterPredicate(LogLevel.error)
b83d422a 749 stdsomething_obs = twisted.logger.FilteringLogObserver(
389236df
IJ
750 stderr_obs, [pred], stdout_obs
751 )
ec2c9312
IJ
752 global file_log_observer
753 file_log_observer = twisted.logger.FilteringLogObserver(
b83d422a
IJ
754 stdsomething_obs, [LogNotBoringTwisted()]
755 )
756 #log_observer = stdsomething_obs
8c3b6620 757 twisted.logger.globalLogBeginner.beginLoggingTo(
ec2c9312 758 [ file_log_observer, crash_on_critical ]
8c3b6620 759 )
ae7c7784 760
ae7c7784 761def common_run():
b68c0739
IJ
762 log_debug(DBG.INIT, 'entering reactor')
763 if not _crashing: reactor.run()
2eecd19c 764 print('ENDED', file=sys.stderr)
207f5042 765 sys.exit(16)