chiark / gitweb /
test-example: all-privkeys: Define, and indirect through, a variable
[secnet.git] / make-secnet-sites
1 #! /usr/bin/env python3
2 #
3 # This file is part of secnet.
4 # See README for full list of copyright holders.
5 #
6 # secnet is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10
11 # secnet is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # version 3 along with secnet; if not, see
18 # https://www.gnu.org/licenses/gpl.html.
19
20 """VPN sites file manipulation.
21
22 This program enables VPN site descriptions to be submitted for
23 inclusion in a central database, and allows the resulting database to
24 be turned into a secnet configuration file.
25
26 A database file can be turned into a secnet configuration file simply:
27 make-secnet-sites.py [infile [outfile]]
28
29 It would be wise to run secnet with the "--just-check-config" option
30 before installing the output on a live system.
31
32 The program expects to be invoked via userv to manage the database; it
33 relies on the USERV_USER and USERV_GROUP environment variables. The
34 command line arguments for this invocation are:
35
36 make-secnet-sites.py -u header-filename groupfiles-directory output-file \
37   group
38
39 All but the last argument are expected to be set by userv; the 'group'
40 argument is provided by the user. A suitable userv configuration file
41 fragment is:
42
43 reset
44 no-disconnect-hup
45 no-suppress-args
46 cd ~/secnet/sites-test/
47 execute ~/secnet/make-secnet-sites.py -u vpnheader groupfiles sites
48
49 This program is part of secnet.
50
51 """
52
53 from __future__ import print_function
54 from __future__ import unicode_literals
55 from builtins import int
56
57 import string
58 import time
59 import sys
60 import os
61 import getopt
62 import re
63 import argparse
64 import math
65
66 import ipaddress
67
68 # entry 0 is "near the executable", or maybe from PYTHONPATH=.,
69 # which we don't want to preempt
70 sys.path.insert(1,"/usr/local/share/secnet")
71 sys.path.insert(1,"/usr/share/secnet")
72 import ipaddrset
73 import base91
74
75 from argparseactionnoyes import ActionNoYes
76
77 VERSION="0.1.18"
78
79 max_version = 2
80
81 from sys import version_info
82 if version_info.major == 2:  # for python2
83     import codecs
84     sys.stdin = codecs.getreader('utf-8')(sys.stdin)
85     sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
86     import io
87     open=lambda f,m='r': io.open(f,m,encoding='utf-8')
88
89 max={'rsa_bits':8200,'name':33,'dh_bits':8200,'algname':127}
90
91 def debugrepr(*args):
92         if debug_level > 0:
93                 print(repr(args), file=sys.stderr)
94
95 def base91s_encode(bindata):
96         return base91.encode(bindata).replace('"',"-")
97
98 def base91s_decode(string):
99         return base91.decode(string.replace("-",'"'))
100
101 class Tainted:
102         def __init__(self,s,tline=None,tfile=None):
103                 self._s=s
104                 self._ok=None
105                 self._line=line if tline is None else tline
106                 self._file=file if tfile is None else tfile
107         def __eq__(self,e):
108                 return self._s==e
109         def __ne__(self,e):
110                 # for Python2
111                 return not self.__eq__(e)
112         def __str__(self):
113                 raise RuntimeError('direct use of Tainted value')
114         def __repr__(self):
115                 return 'Tainted(%s)' % repr(self._s)
116
117         def _bad(self,what,why):
118                 assert(self._ok is not True)
119                 self._ok=False
120                 complain('bad parameter: %s: %s' % (what, why))
121                 return False
122
123         def _max_ok(self,what,maxlen):
124                 if len(self._s) > maxlen:
125                         return self._bad(what,'too long (max %d)' % maxlen)
126                 return True
127
128         def _re_ok(self,bad,what,maxlen=None):
129                 if maxlen is None: maxlen=max[what]
130                 self._max_ok(what,maxlen)
131                 if self._ok is False: return False
132                 if bad.search(self._s):
133                         #print(repr(self), file=sys.stderr)
134                         return self._bad(what,'bad syntax')
135                 return True
136
137         def _rtnval(self, is_ok, ifgood, ifbad=''):
138                 if is_ok:
139                         assert(self._ok is not False)
140                         self._ok=True
141                         return ifgood
142                 else:
143                         assert(self._ok is not True)
144                         self._ok=False
145                         return ifbad
146
147         def _rtn(self, is_ok, ifbad=''):
148                 return self._rtnval(is_ok, self._s, ifbad)
149
150         def raw(self):
151                 return self._s
152         def raw_mark_ok(self):
153                 # caller promises to throw if syntax was dangeorus
154                 return self._rtn(True)
155
156         def output(self):
157                 if self._ok is False: return ''
158                 if self._ok is True: return self._s
159                 print('%s:%d: unchecked/unknown additional data "%s"' %
160                       (self._file,self._line,self._s),
161                       file=sys.stderr)
162                 sys.exit(1)
163
164         bad_name=re.compile(r'^[^a-zA-Z]|[^-_0-9a-zA-Z]')
165         # secnet accepts _ at start of names, but we reserve that
166         bad_name_counter=0
167         def name(self,what='name'):
168                 ok=self._re_ok(Tainted.bad_name,what)
169                 return self._rtn(ok,
170                                  '_line%d_%s' % (self._line, id(self)))
171
172         def keyword(self):
173                 ok=self._s in keywords or self._s in levels
174                 if not ok:
175                         complain('unknown keyword %s' % self._s)
176                 return self._rtn(ok)
177
178         bad_hex=re.compile(r'[^0-9a-fA-F]')
179         def bignum_16(self,kind,what):
180                 maxlen=(max[kind+'_bits']+3)/4
181                 ok=self._re_ok(Tainted.bad_hex,what,maxlen)
182                 return self._rtn(ok)
183
184         bad_num=re.compile(r'[^0-9]')
185         def bignum_10(self,kind,what):
186                 maxlen=math.ceil(max[kind+'_bits'] / math.log10(2))
187                 ok=self._re_ok(Tainted.bad_num,what,maxlen)
188                 return self._rtn(ok)
189
190         def number(self,minn,maxx,what='number'):
191                 # not for bignums
192                 ok=self._re_ok(Tainted.bad_num,what,10)
193                 if ok:
194                         v=int(self._s)
195                         if v<minn or v>maxx:
196                                 ok=self._bad(what,'out of range %d..%d'
197                                              % (minn,maxx))
198                 return self._rtnval(ok,v,minn)
199
200         def hexid(self,byteslen,what):
201                 ok=self._re_ok(Tainted.bad_hex,what,byteslen*2)
202                 if ok:
203                         if len(self._s) < byteslen*2:
204                                 ok=self._bad(what,'too short')
205                 return self._rtn(ok,ifbad='00'*byteslen)
206
207         bad_host=re.compile(r'[^-\][_.:0-9a-zA-Z]')
208         # We permit _ so we can refer to special non-host domains
209         # which have A and AAAA RRs.  This is a crude check and we may
210         # still produce config files with syntactically invalid
211         # domains or addresses, but that is OK.
212         def host(self):
213                 ok=self._re_ok(Tainted.bad_host,'host/address',255)
214                 return self._rtn(ok)
215
216         bad_email=re.compile(r'[^-._0-9a-z@!$%^&*=+~/]')
217         # ^ This does not accept all valid email addresses.  That's
218         # not really possible with this input syntax.  It accepts
219         # all ones that don't require quoting anywhere in email
220         # protocols (and also accepts some invalid ones).
221         def email(self):
222                 ok=self._re_ok(Tainted.bad_email,'email address',1023)
223                 return self._rtn(ok)
224
225         bad_groupname=re.compile(r'^[^_A-Za-z]|[^-+_0-9A-Za-z]')
226         def groupname(self):
227                 ok=self._re_ok(Tainted.bad_groupname,'group name',64)
228                 return self._rtn(ok)
229
230         bad_base91=re.compile(r'[^!-~]|[\'\"\\]')
231         def base91(self,what='base91'):
232                 ok=self._re_ok(Tainted.bad_base91,what,4096)
233                 return self._rtn(ok)
234
235 class ArgActionLambda(argparse.Action):
236         def __init__(self, fn, **kwargs):
237                 self.fn=fn
238                 argparse.Action.__init__(self,**kwargs)
239         def __call__(self,ap,ns,values,option_string):
240                 self.fn(values,ns,ap,option_string)
241
242 class PkmBase():
243         def site_start(self,pubkeys_path):
244                 self._pa=pubkeys_path
245                 self._fs = FilterState()
246         def site_serial(self,serial): pass
247         def write_key(self,k): pass
248         def site_finish(self,confw): pass
249
250 class PkmSingle(PkmBase):
251         opt = 'single'
252         help = 'write one public key per site to sites.conf'
253         def site_start(self,pubkeys_path):
254                 PkmBase.site_start(self,pubkeys_path)
255                 self._outk = []
256         def write_key(self,k):
257                 if k.okforonlykey(output_version,self._fs):
258                         self._outk.append(k)
259         def site_finish(self,confw):
260                 if len(self._outk) == 0:
261                         complain("site with no public key");
262                 elif len(self._outk) != 1:
263                         debugrepr('outk ', self._outk)
264                         complain(
265  "site with multiple public keys, without --pubkeys-install (maybe --output-version=1 would help"
266                         )
267                 else:
268                         confw.write("key %s;\n"%str(self._outk[0]))
269
270 class PkmInstall(PkmBase):
271         opt = 'install'
272         help = 'install public keys in public key directory'
273         def site_start(self,pubkeys_path):
274                 PkmBase.site_start(self,pubkeys_path)
275                 self._pw=open(self._pa+'~tmp','w')
276         def site_serial(self,serial):
277                 self._pw.write('serial %s\n' % serial)
278         def write_key(self,k):
279                 wout=k.forpub(output_version,self._fs)
280                 self._pw.write(' '.join(wout))
281                 self._pw.write('\n')
282         def site_finish(self,confw):
283                 self._pw.close()
284                 os.rename(self._pa+'~tmp',self._pa+'~update')
285                 PkmElide.site_finish(self,confw)
286
287 class PkmElide(PkmBase):
288         opt = 'elide'
289         help = 'no public keys in sites.conf output nor in directory'
290         def site_finish(self,confw):
291                 confw.write("peer-keys \"%s\";\n"%self._pa);
292
293 def parse_args():
294         global service
295         global inputfile
296         global header
297         global groupfiledir
298         global sitesfile
299         global outputfile
300         global group
301         global user
302         global of
303         global prefix
304         global key_prefix
305         global debug_level
306         global output_version
307         global pubkeys_dir
308         global pubkeys_mode
309
310         ap = argparse.ArgumentParser(description='process secnet sites files')
311         ap.add_argument('--userv', '-u', action='store_true',
312                         help='userv service fragment update mode')
313         ap.add_argument('--conf-key-prefix', action=ActionNoYes,
314                         default=True,
315                  help='prefix conf file key names derived from sites data')
316         def add_pkm(how):
317                 ap.add_argument('--pubkeys-'+how().opt, action=ArgActionLambda,
318                         nargs=0,
319                         fn=(lambda v,ns,*x: setattr(ns,'pkm',how)),
320                         help=how().help)
321         add_pkm(PkmInstall)
322         add_pkm(PkmSingle)
323         add_pkm(PkmElide)
324         ap.add_argument('--pubkeys-dir',  nargs=1,
325                         help='public key directory',
326                         default=['/var/lib/secnet/pubkeys'])
327         ap.add_argument('--output-version', nargs=1, type=int,
328                         help='sites file output version',
329                         default=[max_version])
330         ap.add_argument('--prefix', '-P', nargs=1,
331                         help='set prefix')
332         ap.add_argument('--debug', '-D', action='count', default=0)
333         ap.add_argument('arg',nargs=argparse.REMAINDER)
334         av = ap.parse_args()
335         debug_level = av.debug
336         debugrepr('av',av)
337         service = 1 if av.userv else 0
338         prefix = '' if av.prefix is None else av.prefix[0]
339         key_prefix = av.conf_key_prefix
340         output_version = av.output_version[0]
341         pubkeys_dir = av.pubkeys_dir[0]
342         pubkeys_mode = getattr(av,'pkm',PkmSingle)
343         if service:
344                 if len(av.arg)!=4:
345                         print("Wrong number of arguments")
346                         sys.exit(1)
347                 (header, groupfiledir, sitesfile, group) = av.arg
348                 group = Tainted(group,0,'command line')
349                 # untrusted argument from caller
350                 if "USERV_USER" not in os.environ:
351                         print("Environment variable USERV_USER not found")
352                         sys.exit(1)
353                 user=os.environ["USERV_USER"]
354                 # Check that group is in USERV_GROUP
355                 if "USERV_GROUP" not in os.environ:
356                         print("Environment variable USERV_GROUP not found")
357                         sys.exit(1)
358                 ugs=os.environ["USERV_GROUP"]
359                 ok=0
360                 for i in ugs.split():
361                         if group==i: ok=1
362                 if not ok:
363                         print("caller not in group %s"%group)
364                         sys.exit(1)
365         else:
366                 if len(av.arg)>3:
367                         print("Too many arguments")
368                         sys.exit(1)
369                 (inputfile, outputfile) = (av.arg + [None]*2)[0:2]
370
371 parse_args()
372
373 # Classes describing possible datatypes in the configuration file
374
375 class basetype:
376         "Common protocol for configuration types."
377         def add(self,obj,w):
378                 complain("%s %s already has property %s defined"%
379                         (obj.type,obj.name,w[0].raw()))
380         def forsites(self,version,copy,fs):
381                 return copy
382
383 class conflist:
384         "A list of some kind of configuration type."
385         def __init__(self,subtype,w):
386                 self.subtype=subtype
387                 self.list=[subtype(w)]
388         def add(self,obj,w):
389                 self.list.append(self.subtype(w))
390         def __str__(self):
391                 return ', '.join(map(str, self.list))
392         def forsites(self,version,copy,fs):
393                 most_recent=self.list[len(self.list)-1]
394                 return most_recent.forsites(version,copy,fs)
395 def listof(subtype):
396         return lambda w: conflist(subtype, w)
397
398 class single_ipaddr (basetype):
399         "An IP address"
400         def __init__(self,w):
401                 self.addr=ipaddress.ip_address(w[1].raw_mark_ok())
402         def __str__(self):
403                 return '"%s"'%self.addr
404
405 class networks (basetype):
406         "A set of IP addresses specified as a list of networks"
407         def __init__(self,w):
408                 self.set=ipaddrset.IPAddressSet()
409                 for i in w[1:]:
410                         x=ipaddress.ip_network(i.raw_mark_ok(),strict=True)
411                         self.set.append([x])
412         def __str__(self):
413                 return ",".join(map((lambda n: '"%s"'%n), self.set.networks()))
414
415 class dhgroup (basetype):
416         "A Diffie-Hellman group"
417         def __init__(self,w):
418                 self.mod=w[1].bignum_16('dh','dh mod')
419                 self.gen=w[2].bignum_16('dh','dh gen')
420         def __str__(self):
421                 return 'diffie-hellman("%s","%s")'%(self.mod,self.gen)
422
423 class hash (basetype):
424         "A choice of hash function"
425         def __init__(self,w):
426                 hname=w[1]
427                 self.ht=hname.raw()
428                 if (self.ht!='md5' and self.ht!='sha1'):
429                         complain("unknown hash type %s"%(self.ht))
430                         self.ht=None
431                 else:
432                         hname.raw_mark_ok()
433         def __str__(self):
434                 return '%s'%(self.ht)
435
436 class email (basetype):
437         "An email address"
438         def __init__(self,w):
439                 self.addr=w[1].email()
440         def __str__(self):
441                 return '<%s>'%(self.addr)
442
443 class boolean (basetype):
444         "A boolean"
445         def __init__(self,w):
446                 v=w[1]
447                 if re.match('[TtYy1]',v.raw()):
448                         self.b=True
449                         v.raw_mark_ok()
450                 elif re.match('[FfNn0]',v.raw()):
451                         self.b=False
452                         v.raw_mark_ok()
453                 else:
454                         complain("invalid boolean value");
455         def __str__(self):
456                 return ['False','True'][self.b]
457
458 class num (basetype):
459         "A decimal number"
460         def __init__(self,w):
461                 self.n=w[1].number(0,0x7fffffff)
462         def __str__(self):
463                 return '%d'%(self.n)
464
465 class serial (basetype):
466         def __init__(self,w):
467                 self.i=w[1].hexid(4,'serial')
468         def __str__(self):
469                 return self.i
470         def forsites(self,version,copy,fs):
471                 if version < 2: return []
472                 return copy
473
474 class address (basetype):
475         "A DNS name and UDP port number"
476         def __init__(self,w):
477                 self.adr=w[1].host()
478                 self.port=w[2].number(1,65536,'port')
479         def __str__(self):
480                 return '"%s"; port %d'%(self.adr,self.port)
481
482 class inpub (basetype):
483         def forsites(self,version,xcopy,fs):
484                 return self.forpub(version,fs)
485
486 class pubkey (inpub):
487         "Some kind of publie key"
488         def __init__(self,w):
489                 self.a=w[1].name('algname')
490                 self.d=w[2].base91();
491         def __str__(self):
492                 return 'make-public("%s","%s")'%(self.a,self.d)
493         def forpub(self,version,fs):
494                 if version < 2: return []
495                 return ['pub', self.a, self.d]
496         def okforonlykey(self,version,fs):
497                 return len(self.forpub(version,fs)) != 0
498
499 class rsakey (pubkey):
500         "An old-style RSA public key"
501         def __init__(self,w):
502                 self.l=w[1].number(0,max['rsa_bits'],'rsa len')
503                 self.e=w[2].bignum_10('rsa','rsa e')
504                 self.n=w[3].bignum_10('rsa','rsa n')
505                 if len(w) >= 5: w[4].email()
506                 self.a='rsa1'
507                 self.d=base91s_encode(b'%d %s %s' %
508                                       (self.l,
509                                        self.e.encode('ascii'),
510                                        self.n.encode('ascii')))
511                 # ^ this allows us to use the pubkey.forsites()
512                 # method for output in versions>=2
513         def __str__(self):
514                 return 'rsa-public("%s","%s")'%(self.e,self.n)
515                 # this specialisation means we can generate files
516                 # compatible with old secnet executables
517         def forpub(self,version,fs):
518                 if version < 2:
519                         if fs.pkg != '00000000': return []
520                         return ['pubkey', str(self.l), self.e, self.n]
521                 return pubkey.forpub(self,version,fs)
522
523 class rsakey_newfmt(rsakey):
524         "An old-style RSA public key in new-style sites format"
525         # This is its own class simply to have its own constructor.
526         def __init__(self,w):
527                 self.a=w[1].name()
528                 assert(self.a == 'rsa1')
529                 self.d=w[2].base91()
530                 try:
531                         w_inner=list(map(Tainted,
532                                         ['X-PUB-RSA1'] +
533                                         base91s_decode(self.d)
534                                         .decode('ascii')
535                                         .split(' ')))
536                 except UnicodeDecodeError:
537                         complain('rsa1 key in new format has bad base91')
538                 #print(repr(w_inner), file=sys.stderr)
539                 rsakey.__init__(self,w_inner)
540
541 class pubkey_group(inpub):
542         "Public key group introducer"
543         # appears in the site's list of keys mixed in with the keys
544         def __init__(self,w,fallback):
545                 self.i=w[1].hexid(4,'pkg-id')
546                 self.fallback=fallback
547         def forpub(self,version,fs):
548                 fs.pkg=self.i
549                 if version < 2: return []
550                 return ['pkgf' if self.fallback else 'pkg', self.i]
551         def okforonlykey(self,version,fs):
552                 self.forpub(version,fs)
553                 return False
554         
555 def somepubkey(w):
556         #print(repr(w), file=sys.stderr)
557         if w[0]=='pubkey':
558                 return rsakey(w)
559         elif w[0]=='pub' and w[1]=='rsa1':
560                 return rsakey_newfmt(w)
561         elif w[0]=='pub':
562                 return pubkey(w)
563         elif w[0]=='pkg':
564                 return pubkey_group(w,False)
565         elif w[0]=='pkgf':
566                 return pubkey_group(w,True)
567         else:
568                 assert(False)
569
570 # Possible properties of configuration nodes
571 keywords={
572  'contact':(email,"Contact address"),
573  'dh':(dhgroup,"Diffie-Hellman group"),
574  'hash':(hash,"Hash function"),
575  'key-lifetime':(num,"Maximum key lifetime (ms)"),
576  'setup-timeout':(num,"Key setup timeout (ms)"),
577  'setup-retries':(num,"Maximum key setup packet retries"),
578  'wait-time':(num,"Time to wait after unsuccessful key setup (ms)"),
579  'renegotiate-time':(num,"Time after key setup to begin renegotiation (ms)"),
580  'restrict-nets':(networks,"Allowable networks"),
581  'networks':(networks,"Claimed networks"),
582  'serial':(serial,"public key set serial"),
583  'pkg':(listof(somepubkey),"start of public key group",'pub'),
584  'pkgf':(listof(somepubkey),"start of fallback public key group",'pub'),
585  'pub':(listof(somepubkey),"new style public site key"),
586  'pubkey':(listof(somepubkey),"Old-style RSA public site key",'pub'),
587  'peer':(single_ipaddr,"Tunnel peer IP address"),
588  'address':(address,"External contact address and port"),
589  'mobile':(boolean,"Site is mobile"),
590 }
591
592 def sp(name,value):
593         "Simply output a property - the default case"
594         return "%s %s;\n"%(name,value)
595
596 # All levels support these properties
597 global_properties={
598         'contact':(lambda name,value:"# Contact email address: %s\n"%(value)),
599         'dh':sp,
600         'hash':sp,
601         'key-lifetime':sp,
602         'setup-timeout':sp,
603         'setup-retries':sp,
604         'wait-time':sp,
605         'renegotiate-time':sp,
606         'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value),
607 }
608
609 class level:
610         "A level in the configuration hierarchy"
611         depth=0
612         leaf=0
613         allow_properties={}
614         require_properties={}
615         def __init__(self,w):
616                 self.type=w[0].keyword()
617                 self.name=w[1].name()
618                 self.properties={}
619                 self.children={}
620         def indent(self,w,t):
621                 w.write("                 "[:t])
622         def prop_out(self,n):
623                 return self.allow_properties[n](n,str(self.properties[n]))
624         def output_props(self,w,ind):
625                 for i in sorted(self.properties.keys()):
626                         if self.allow_properties[i]:
627                                 self.indent(w,ind)
628                                 w.write("%s"%self.prop_out(i))
629         def kname(self):
630                 return ((self.type[0].upper() if key_prefix else '')
631                         + self.name)
632         def output_data(self,w,path):
633                 ind = 2*len(path)
634                 self.indent(w,ind)
635                 w.write("%s {\n"%(self.kname()))
636                 self.output_props(w,ind+2)
637                 if self.depth==1: w.write("\n");
638                 for k in sorted(self.children.keys()):
639                         c=self.children[k]
640                         c.output_data(w,path+(c,))
641                 self.indent(w,ind)
642                 w.write("};\n")
643
644 class vpnlevel(level):
645         "VPN level in the configuration hierarchy"
646         depth=1
647         leaf=0
648         type="vpn"
649         allow_properties=global_properties.copy()
650         require_properties={
651          'contact':"VPN admin contact address"
652         }
653         def __init__(self,w):
654                 level.__init__(self,w)
655         def output_vpnflat(self,w,path):
656                 "Output flattened list of site names for this VPN"
657                 ind=2*(len(path)+1)
658                 self.indent(w,ind)
659                 w.write("%s {\n"%(self.kname()))
660                 for i in self.children.keys():
661                         self.children[i].output_vpnflat(w,path+(self,))
662                 w.write("\n")
663                 self.indent(w,ind+2)
664                 w.write("all-sites %s;\n"%
665                         ','.join(map(lambda i: i.kname(),
666                                      self.children.values())))
667                 self.indent(w,ind)
668                 w.write("};\n")
669
670 class locationlevel(level):
671         "Location level in the configuration hierarchy"
672         depth=2
673         leaf=0
674         type="location"
675         allow_properties=global_properties.copy()
676         require_properties={
677          'contact':"Location admin contact address",
678         }
679         def __init__(self,w):
680                 level.__init__(self,w)
681                 self.group=w[2].groupname()
682         def output_vpnflat(self,w,path):
683                 ind=2*(len(path)+1)
684                 self.indent(w,ind)
685                 # The "path=path,self=self" abomination below exists because
686                 # Python didn't support nested_scopes until version 2.1
687                 #
688                 #"/"+self.name+"/"+i
689                 w.write("%s %s;\n"%(self.kname(),','.join(
690                         map(lambda x,path=path,self=self:
691                             '/'.join([prefix+"vpn-data"] + list(map(
692                                     lambda i: i.kname(),
693                                     path+(self,x)))),
694                             self.children.values()))))
695
696 class sitelevel(level):
697         "Site level (i.e. a leafnode) in the configuration hierarchy"
698         depth=3
699         leaf=1
700         type="site"
701         allow_properties=global_properties.copy()
702         allow_properties.update({
703          'address':sp,
704          'networks':None,
705          'peer':None,
706          'serial':None,
707          'pkg':None,
708          'pkgf':None,
709          'pub':None,
710          'pubkey':None,
711          'mobile':sp,
712         })
713         require_properties={
714          'dh':"Diffie-Hellman group",
715          'contact':"Site admin contact address",
716          'networks':"Networks claimed by the site",
717          'hash':"hash function",
718          'peer':"Gateway address of the site",
719         }
720         def mangle_name(self):
721                 return self.name.replace('/',',')
722         def pubkeys_path(self):
723                 return pubkeys_dir + '/peer.' + self.mangle_name()
724         def __init__(self,w):
725                 level.__init__(self,w)
726         def output_data(self,w,path):
727                 ind=2*len(path)
728                 np='/'.join(map(lambda i: i.name, path))
729                 self.indent(w,ind)
730                 w.write("%s {\n"%(self.kname()))
731                 self.indent(w,ind+2)
732                 w.write("name \"%s\";\n"%(np,))
733                 self.indent(w,ind+2)
734
735                 pkm = pubkeys_mode()
736                 debugrepr('pkm ',pkm)
737                 pkm.site_start(self.pubkeys_path())
738                 if 'serial' in self.properties:
739                         pkm.site_serial(self.properties['serial'])
740
741                 for k in self.properties["pub"].list:
742                         debugrepr('pubkeys ', k)
743                         pkm.write_key(k)
744
745                 pkm.site_finish(w)
746
747                 self.output_props(w,ind+2)
748                 self.indent(w,ind+2)
749                 w.write("link netlink {\n");
750                 self.indent(w,ind+4)
751                 w.write("routes %s;\n"%str(self.properties["networks"]))
752                 self.indent(w,ind+4)
753                 w.write("ptp-address %s;\n"%str(self.properties["peer"]))
754                 self.indent(w,ind+2)
755                 w.write("};\n")
756                 self.indent(w,ind)
757                 w.write("};\n")
758
759 # Levels in the configuration file
760 # (depth,properties)
761 levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
762
763 def complain(msg):
764         "Complain about a particular input line"
765         moan(("%s line %d: "%(file,line))+msg)
766 def moan(msg):
767         "Complain about something in general"
768         global complaints
769         print(msg);
770         if complaints is None: sys.exit(1)
771         complaints=complaints+1
772
773 class UntaintedRoot():
774         def __init__(self,s): self._s=s
775         def name(self): return self._s
776         def keyword(self): return self._s
777
778 root=level([UntaintedRoot(x) for x in ['root','root']])
779 # All vpns are children of this node
780 obstack=[root]
781 allow_defs=0   # Level above which new definitions are permitted
782
783 def set_property(obj,w):
784         "Set a property on a configuration node"
785         prop=w[0]
786         propname=prop.raw_mark_ok()
787         kw=keywords[propname]
788         if len(kw) >= 3: propname=kw[2] # for aliases
789         if propname in obj.properties:
790                 obj.properties[propname].add(obj,w)
791         else:
792                 obj.properties[propname]=kw[0](w)
793         return obj.properties[propname]
794
795 class FilterState:
796         def __init__(self):
797                 self.reset()
798         def reset(self):
799                 # called when we enter a new node,
800                 # in particular, at the start of each site
801                 self.pkg = '00000000'
802
803 def pline(il,filterstate,allow_include=False):
804         "Process a configuration file line"
805         global allow_defs, obstack, root
806         w=il.rstrip('\n').split()
807         if len(w)==0: return ['']
808         w=list([Tainted(x) for x in w])
809         keyword=w[0]
810         current=obstack[len(obstack)-1]
811         copyout_core=lambda: ' '.join([ww.output() for ww in w])
812         indent='    '*len(obstack)
813         copyout=lambda: [indent + copyout_core() + '\n']
814         if keyword=='end-definitions':
815                 keyword.raw_mark_ok()
816                 allow_defs=sitelevel.depth
817                 obstack=[root]
818                 return copyout()
819         if keyword=='include':
820                 if not allow_include:
821                         complain("include not permitted here")
822                         return []
823                 if len(w) != 2:
824                         complain("include requires one argument")
825                         return []
826                 newfile=os.path.join(os.path.dirname(file),w[1].raw_mark_ok())
827                 # ^ user of "include" is trusted so raw_mark_ok is good
828                 return pfilepath(newfile,allow_include=allow_include)
829         if keyword.raw() in levels:
830                 # We may go up any number of levels, but only down by one
831                 newdepth=levels[keyword.raw_mark_ok()].depth
832                 currentdepth=len(obstack) # actually +1...
833                 if newdepth<=currentdepth:
834                         obstack=obstack[:newdepth]
835                 if newdepth>currentdepth:
836                         complain("May not go from level %d to level %d"%
837                                 (currentdepth-1,newdepth))
838                 # See if it's a new one (and whether that's permitted)
839                 # or an existing one
840                 current=obstack[len(obstack)-1]
841                 tname=w[1].name()
842                 if tname in current.children:
843                         # Not new
844                         current=current.children[tname]
845                         if service and group and current.depth==2:
846                                 if group!=current.group:
847                                         complain("Incorrect group!")
848                                 w[2].groupname()
849                 else:
850                         # New
851                         # Ignore depth check for now
852                         nl=levels[keyword.raw()](w)
853                         if nl.depth<allow_defs:
854                                 complain("New definitions not allowed at "
855                                         "level %d"%nl.depth)
856                                 # we risk crashing if we continue
857                                 sys.exit(1)
858                         current.children[tname]=nl
859                         current=nl
860                 filterstate.reset()
861                 obstack.append(current)
862                 return copyout()
863         if keyword.raw() not in current.allow_properties:
864                 complain("Property %s not allowed at %s level"%
865                         (keyword.raw(),current.type))
866                 return []
867         elif current.depth == vpnlevel.depth < allow_defs:
868                 complain("Not allowed to set VPN properties here")
869                 return []
870         else:
871                 prop=set_property(current,w)
872                 out=[copyout_core()]
873                 out=prop.forsites(output_version,out,filterstate)
874                 if len(out)==0: return [indent + '#', copyout_core(), '\n']
875                 return [indent + ' '.join(out) + '\n']
876
877         complain("unknown keyword '%s'"%(keyword.raw()))
878
879 def pfilepath(pathname,allow_include=False):
880         f=open(pathname)
881         outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
882         f.close()
883         return outlines
884
885 def pfile(name,lines,allow_include=False):
886         "Process a file"
887         global file,line
888         file=name
889         line=0
890         outlines=[]
891         filterstate = FilterState()
892         for i in lines:
893                 line=line+1
894                 if (i[0]=='#'): continue
895                 outlines += pline(i,filterstate,allow_include=allow_include)
896         return outlines
897
898 def outputsites(w):
899         "Output include file for secnet configuration"
900         w.write("# secnet sites file autogenerated by make-secnet-sites "
901                 +"version %s\n"%VERSION)
902         w.write("# %s\n"%time.asctime(time.localtime(time.time())))
903         w.write("# Command line: %s\n\n"%' '.join(sys.argv))
904
905         # Raw VPN data section of file
906         w.write(prefix+"vpn-data {\n")
907         for i in root.children.values():
908                 i.output_data(w,(i,))
909         w.write("};\n")
910
911         # Per-VPN flattened lists
912         w.write(prefix+"vpn {\n")
913         for i in root.children.values():
914                 i.output_vpnflat(w,())
915         w.write("};\n")
916
917         # Flattened list of sites
918         w.write(prefix+"all-sites %s;\n"%",".join(
919                 map(lambda x:"%svpn/%s/all-sites"%(prefix,x.kname()),
920                         root.children.values())))
921
922 line=0
923 file=None
924 complaints=0
925
926 # Sanity check section
927 # Delete nodes where leaf=0 that have no children
928
929 def live(n):
930         "Number of leafnodes below node n"
931         if n.leaf: return 1
932         for i in n.children.keys():
933                 if live(n.children[i]): return 1
934         return 0
935 def delempty(n):
936         "Delete nodes that have no leafnode children"
937         for i in list(n.children.keys()):
938                 delempty(n.children[i])
939                 if not live(n.children[i]):
940                         del n.children[i]
941
942 # Check that all constraints are met (as far as I can tell
943 # restrict-nets/networks/peer are the only special cases)
944
945 def checkconstraints(n,p,ra):
946         new_p=p.copy()
947         new_p.update(n.properties)
948         for i in n.require_properties.keys():
949                 if i not in new_p:
950                         moan("%s %s is missing property %s"%
951                                 (n.type,n.name,i))
952         for i in new_p.keys():
953                 if i not in n.allow_properties:
954                         moan("%s %s has forbidden property %s"%
955                                 (n.type,n.name,i))
956         # Check address range restrictions
957         if "restrict-nets" in n.properties:
958                 new_ra=ra.intersection(n.properties["restrict-nets"].set)
959         else:
960                 new_ra=ra
961         if "networks" in n.properties:
962                 if not n.properties["networks"].set <= new_ra:
963                         moan("%s %s networks out of bounds"%(n.type,n.name))
964                 if "peer" in n.properties:
965                         if not n.properties["networks"].set.contains(
966                                 n.properties["peer"].addr):
967                                 moan("%s %s peer not in networks"%(n.type,n.name))
968         for i in n.children.keys():
969                 checkconstraints(n.children[i],new_p,new_ra)
970
971 if service:
972         headerinput=pfilepath(header,allow_include=True)
973         userinput=sys.stdin.readlines()
974         pfile("user input",userinput)
975 else:
976         if inputfile is None:
977                 pfile("stdin",sys.stdin.readlines())
978         else:
979                 pfilepath(inputfile)
980
981 delempty(root)
982 checkconstraints(root,{},ipaddrset.complete_set())
983
984 if complaints>0:
985         if complaints==1: print("There was 1 problem.")
986         else: print("There were %d problems."%(complaints))
987         sys.exit(1)
988 complaints=None # arranges to crash if we complain later
989
990 if service:
991         # Put the user's input into their group file, and rebuild the main
992         # sites file
993         f=open(groupfiledir+"/T"+group.groupname(),'w')
994         f.write("# Section submitted by user %s, %s\n"%
995                 (user,time.asctime(time.localtime(time.time()))))
996         f.write("# Checked by make-secnet-sites version %s\n\n"%VERSION)
997         for i in userinput: f.write(i)
998         f.write("\n")
999         f.close()
1000         os.rename(groupfiledir+"/T"+group.groupname(),
1001                   groupfiledir+"/R"+group.groupname())
1002         f=open(sitesfile+"-tmp",'w')
1003         f.write("# sites file autogenerated by make-secnet-sites\n")
1004         f.write("# generated %s, invoked by %s\n"%
1005                 (time.asctime(time.localtime(time.time())),user))
1006         f.write("# use make-secnet-sites to turn this file into a\n")
1007         f.write("# valid /etc/secnet/sites.conf file\n\n")
1008         for i in headerinput: f.write(i)
1009         files=os.listdir(groupfiledir)
1010         for i in files:
1011                 if i[0]=='R':
1012                         j=open(groupfiledir+"/"+i)
1013                         f.write(j.read())
1014                         j.close()
1015         f.write("# end of sites file\n")
1016         f.close()
1017         os.rename(sitesfile+"-tmp",sitesfile)
1018 else:
1019         if outputfile is None:
1020                 of=sys.stdout
1021         else:
1022                 tmp_outputfile=outputfile+'~tmp~'
1023                 of=open(tmp_outputfile,'w')
1024         outputsites(of)
1025         if outputfile is not None:
1026                 os.rename(tmp_outputfile,outputfile)