chiark / gitweb /
make-secnet-sites: Add new pubkeys-dir option, and pubkey paths
[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 def parse_args():
243         global service
244         global inputfile
245         global header
246         global groupfiledir
247         global sitesfile
248         global outputfile
249         global group
250         global user
251         global of
252         global prefix
253         global key_prefix
254         global debug_level
255         global output_version
256         global pubkeys_dir
257
258         ap = argparse.ArgumentParser(description='process secnet sites files')
259         ap.add_argument('--userv', '-u', action='store_true',
260                         help='userv service fragment update mode')
261         ap.add_argument('--conf-key-prefix', action=ActionNoYes,
262                         default=True,
263                  help='prefix conf file key names derived from sites data')
264         ap.add_argument('--pubkeys-dir',  nargs=1,
265                         help='public key directory',
266                         default=['/var/lib/secnet/pubkeys'])
267         ap.add_argument('--output-version', nargs=1, type=int,
268                         help='sites file output version',
269                         default=[max_version])
270         ap.add_argument('--prefix', '-P', nargs=1,
271                         help='set prefix')
272         ap.add_argument('--debug', '-D', action='count', default=0)
273         ap.add_argument('arg',nargs=argparse.REMAINDER)
274         av = ap.parse_args()
275         debug_level = av.debug
276         debugrepr('av',av)
277         service = 1 if av.userv else 0
278         prefix = '' if av.prefix is None else av.prefix[0]
279         key_prefix = av.conf_key_prefix
280         output_version = av.output_version[0]
281         pubkeys_dir = av.pubkeys_dir[0]
282         if service:
283                 if len(av.arg)!=4:
284                         print("Wrong number of arguments")
285                         sys.exit(1)
286                 (header, groupfiledir, sitesfile, group) = av.arg
287                 group = Tainted(group,0,'command line')
288                 # untrusted argument from caller
289                 if "USERV_USER" not in os.environ:
290                         print("Environment variable USERV_USER not found")
291                         sys.exit(1)
292                 user=os.environ["USERV_USER"]
293                 # Check that group is in USERV_GROUP
294                 if "USERV_GROUP" not in os.environ:
295                         print("Environment variable USERV_GROUP not found")
296                         sys.exit(1)
297                 ugs=os.environ["USERV_GROUP"]
298                 ok=0
299                 for i in ugs.split():
300                         if group==i: ok=1
301                 if not ok:
302                         print("caller not in group %s"%group)
303                         sys.exit(1)
304         else:
305                 if len(av.arg)>3:
306                         print("Too many arguments")
307                         sys.exit(1)
308                 (inputfile, outputfile) = (av.arg + [None]*2)[0:2]
309
310 parse_args()
311
312 # Classes describing possible datatypes in the configuration file
313
314 class basetype:
315         "Common protocol for configuration types."
316         def add(self,obj,w):
317                 complain("%s %s already has property %s defined"%
318                         (obj.type,obj.name,w[0].raw()))
319         def forsites(self,version,copy,fs):
320                 return copy
321
322 class conflist:
323         "A list of some kind of configuration type."
324         def __init__(self,subtype,w):
325                 self.subtype=subtype
326                 self.list=[subtype(w)]
327         def add(self,obj,w):
328                 self.list.append(self.subtype(w))
329         def __str__(self):
330                 return ', '.join(map(str, self.list))
331         def forsites(self,version,copy,fs):
332                 most_recent=self.list[len(self.list)-1]
333                 return most_recent.forsites(version,copy,fs)
334 def listof(subtype):
335         return lambda w: conflist(subtype, w)
336
337 class single_ipaddr (basetype):
338         "An IP address"
339         def __init__(self,w):
340                 self.addr=ipaddress.ip_address(w[1].raw_mark_ok())
341         def __str__(self):
342                 return '"%s"'%self.addr
343
344 class networks (basetype):
345         "A set of IP addresses specified as a list of networks"
346         def __init__(self,w):
347                 self.set=ipaddrset.IPAddressSet()
348                 for i in w[1:]:
349                         x=ipaddress.ip_network(i.raw_mark_ok(),strict=True)
350                         self.set.append([x])
351         def __str__(self):
352                 return ",".join(map((lambda n: '"%s"'%n), self.set.networks()))
353
354 class dhgroup (basetype):
355         "A Diffie-Hellman group"
356         def __init__(self,w):
357                 self.mod=w[1].bignum_16('dh','dh mod')
358                 self.gen=w[2].bignum_16('dh','dh gen')
359         def __str__(self):
360                 return 'diffie-hellman("%s","%s")'%(self.mod,self.gen)
361
362 class hash (basetype):
363         "A choice of hash function"
364         def __init__(self,w):
365                 hname=w[1]
366                 self.ht=hname.raw()
367                 if (self.ht!='md5' and self.ht!='sha1'):
368                         complain("unknown hash type %s"%(self.ht))
369                         self.ht=None
370                 else:
371                         hname.raw_mark_ok()
372         def __str__(self):
373                 return '%s'%(self.ht)
374
375 class email (basetype):
376         "An email address"
377         def __init__(self,w):
378                 self.addr=w[1].email()
379         def __str__(self):
380                 return '<%s>'%(self.addr)
381
382 class boolean (basetype):
383         "A boolean"
384         def __init__(self,w):
385                 v=w[1]
386                 if re.match('[TtYy1]',v.raw()):
387                         self.b=True
388                         v.raw_mark_ok()
389                 elif re.match('[FfNn0]',v.raw()):
390                         self.b=False
391                         v.raw_mark_ok()
392                 else:
393                         complain("invalid boolean value");
394         def __str__(self):
395                 return ['False','True'][self.b]
396
397 class num (basetype):
398         "A decimal number"
399         def __init__(self,w):
400                 self.n=w[1].number(0,0x7fffffff)
401         def __str__(self):
402                 return '%d'%(self.n)
403
404 class serial (basetype):
405         def __init__(self,w):
406                 self.i=w[1].hexid(4,'serial')
407         def __str__(self):
408                 return self.i
409         def forsites(self,version,copy,fs):
410                 if version < 2: return []
411                 return copy
412
413 class address (basetype):
414         "A DNS name and UDP port number"
415         def __init__(self,w):
416                 self.adr=w[1].host()
417                 self.port=w[2].number(1,65536,'port')
418         def __str__(self):
419                 return '"%s"; port %d'%(self.adr,self.port)
420
421 class pubkey (basetype):
422         "Some kind of publie key"
423         def __init__(self,w):
424                 self.a=w[1].name('algname')
425                 self.d=w[2].base91();
426         def __str__(self):
427                 return 'make-public("%s","%s")'%(self.a,self.d)
428         def forsites(self,version,xcopy,fs):
429                 if version < 2: return []
430                 return ['pub', self.a, self.d]
431
432 class rsakey (pubkey):
433         "An RSA public key"
434         def __init__(self,w):
435                 self.l=w[1].number(0,max['rsa_bits'],'rsa len')
436                 self.e=w[2].bignum_10('rsa','rsa e')
437                 self.n=w[3].bignum_10('rsa','rsa n')
438                 if len(w) >= 5: w[4].email()
439                 self.a='rsa1'
440                 self.d=base91s_encode(b'%d %s %s' %
441                                       (self.l,
442                                        self.e.encode('ascii'),
443                                        self.n.encode('ascii')))
444                 # ^ this allows us to use the pubkey.forsites()
445                 # method for output in versions>=2
446         def __str__(self):
447                 return 'rsa-public("%s","%s")'%(self.e,self.n)
448                 # this specialisation means we can generate files
449                 # compatible with old secnet executables
450         def forsites(self,version,xcopy,fs):
451                 if version < 2:
452                         return ['pubkey', str(self.l), self.e, self.n]
453                 return pubkey.forsites(self,version,xcopy,fs)
454
455 class rsakey_newfmt(rsakey):
456         "An old-style RSA public key in new-style sites format"
457         # This is its own class simply to have its own constructor.
458         def __init__(self,w):
459                 self.a=w[1].name()
460                 assert(self.a == 'rsa1')
461                 self.d=w[2].base91()
462                 try:
463                         w_inner=list(map(Tainted,
464                                         ['X-PUB-RSA1'] +
465                                         base91s_decode(self.d)
466                                         .decode('ascii')
467                                         .split(' ')))
468                 except UnicodeDecodeError:
469                         complain('rsa1 key in new format has bad base91')
470                 #print(repr(w_inner), file=sys.stderr)
471                 rsakey.__init__(self,w_inner)
472 def somepubkey(w):
473         if w[0]=='pubkey':
474                 return rsakey(w)
475         elif w[0]=='pub' and w[1]=='rsa1':
476                 return rsakey_newfmt(w)
477         elif w[0]=='pub':
478                 return pubkey(w)
479         else:
480                 assert(False)
481
482 # Possible properties of configuration nodes
483 keywords={
484  'contact':(email,"Contact address"),
485  'dh':(dhgroup,"Diffie-Hellman group"),
486  'hash':(hash,"Hash function"),
487  'key-lifetime':(num,"Maximum key lifetime (ms)"),
488  'setup-timeout':(num,"Key setup timeout (ms)"),
489  'setup-retries':(num,"Maximum key setup packet retries"),
490  'wait-time':(num,"Time to wait after unsuccessful key setup (ms)"),
491  'renegotiate-time':(num,"Time after key setup to begin renegotiation (ms)"),
492  'restrict-nets':(networks,"Allowable networks"),
493  'networks':(networks,"Claimed networks"),
494  'pub':(listof(somepubkey),"new style public site key"),
495  'pubkey':(listof(somepubkey),"RSA public site key",'pub'),
496  'peer':(single_ipaddr,"Tunnel peer IP address"),
497  'address':(address,"External contact address and port"),
498  'mobile':(boolean,"Site is mobile"),
499 }
500
501 def sp(name,value):
502         "Simply output a property - the default case"
503         return "%s %s;\n"%(name,value)
504
505 # All levels support these properties
506 global_properties={
507         'contact':(lambda name,value:"# Contact email address: %s\n"%(value)),
508         'dh':sp,
509         'hash':sp,
510         'key-lifetime':sp,
511         'setup-timeout':sp,
512         'setup-retries':sp,
513         'wait-time':sp,
514         'renegotiate-time':sp,
515         'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value),
516 }
517
518 class level:
519         "A level in the configuration hierarchy"
520         depth=0
521         leaf=0
522         allow_properties={}
523         require_properties={}
524         def __init__(self,w):
525                 self.type=w[0].keyword()
526                 self.name=w[1].name()
527                 self.properties={}
528                 self.children={}
529         def indent(self,w,t):
530                 w.write("                 "[:t])
531         def prop_out(self,n):
532                 return self.allow_properties[n](n,str(self.properties[n]))
533         def output_props(self,w,ind):
534                 for i in sorted(self.properties.keys()):
535                         if self.allow_properties[i]:
536                                 self.indent(w,ind)
537                                 w.write("%s"%self.prop_out(i))
538         def kname(self):
539                 return ((self.type[0].upper() if key_prefix else '')
540                         + self.name)
541         def output_data(self,w,path):
542                 ind = 2*len(path)
543                 self.indent(w,ind)
544                 w.write("%s {\n"%(self.kname()))
545                 self.output_props(w,ind+2)
546                 if self.depth==1: w.write("\n");
547                 for k in sorted(self.children.keys()):
548                         c=self.children[k]
549                         c.output_data(w,path+(c,))
550                 self.indent(w,ind)
551                 w.write("};\n")
552
553 class vpnlevel(level):
554         "VPN level in the configuration hierarchy"
555         depth=1
556         leaf=0
557         type="vpn"
558         allow_properties=global_properties.copy()
559         require_properties={
560          'contact':"VPN admin contact address"
561         }
562         def __init__(self,w):
563                 level.__init__(self,w)
564         def output_vpnflat(self,w,path):
565                 "Output flattened list of site names for this VPN"
566                 ind=2*(len(path)+1)
567                 self.indent(w,ind)
568                 w.write("%s {\n"%(self.kname()))
569                 for i in self.children.keys():
570                         self.children[i].output_vpnflat(w,path+(self,))
571                 w.write("\n")
572                 self.indent(w,ind+2)
573                 w.write("all-sites %s;\n"%
574                         ','.join(map(lambda i: i.kname(),
575                                      self.children.values())))
576                 self.indent(w,ind)
577                 w.write("};\n")
578
579 class locationlevel(level):
580         "Location level in the configuration hierarchy"
581         depth=2
582         leaf=0
583         type="location"
584         allow_properties=global_properties.copy()
585         require_properties={
586          'contact':"Location admin contact address",
587         }
588         def __init__(self,w):
589                 level.__init__(self,w)
590                 self.group=w[2].groupname()
591         def output_vpnflat(self,w,path):
592                 ind=2*(len(path)+1)
593                 self.indent(w,ind)
594                 # The "path=path,self=self" abomination below exists because
595                 # Python didn't support nested_scopes until version 2.1
596                 #
597                 #"/"+self.name+"/"+i
598                 w.write("%s %s;\n"%(self.kname(),','.join(
599                         map(lambda x,path=path,self=self:
600                             '/'.join([prefix+"vpn-data"] + list(map(
601                                     lambda i: i.kname(),
602                                     path+(self,x)))),
603                             self.children.values()))))
604
605 class sitelevel(level):
606         "Site level (i.e. a leafnode) in the configuration hierarchy"
607         depth=3
608         leaf=1
609         type="site"
610         allow_properties=global_properties.copy()
611         allow_properties.update({
612          'address':sp,
613          'networks':None,
614          'peer':None,
615          'pub':None,
616          'pubkey':None,
617          'mobile':sp,
618         })
619         require_properties={
620          'dh':"Diffie-Hellman group",
621          'contact':"Site admin contact address",
622          'networks':"Networks claimed by the site",
623          'hash':"hash function",
624          'peer':"Gateway address of the site",
625          'pub':"public key of the site",
626         }
627         def mangle_name(self):
628                 return self.name.replace('/',',')
629         def pubkeys_path(self):
630                 return pubkeys_dir + '/peer.' + self.mangle_name()
631         def __init__(self,w):
632                 level.__init__(self,w)
633         def output_data(self,w,path):
634                 ind=2*len(path)
635                 np='/'.join(map(lambda i: i.name, path))
636                 self.indent(w,ind)
637                 w.write("%s {\n"%(self.kname()))
638                 self.indent(w,ind+2)
639                 w.write("name \"%s\";\n"%(np,))
640                 self.indent(w,ind+2)
641                 w.write("key %s;\n"%str(self.properties["pub"].list[0]))
642                 self.output_props(w,ind+2)
643                 self.indent(w,ind+2)
644                 w.write("link netlink {\n");
645                 self.indent(w,ind+4)
646                 w.write("routes %s;\n"%str(self.properties["networks"]))
647                 self.indent(w,ind+4)
648                 w.write("ptp-address %s;\n"%str(self.properties["peer"]))
649                 self.indent(w,ind+2)
650                 w.write("};\n")
651                 self.indent(w,ind)
652                 w.write("};\n")
653
654 # Levels in the configuration file
655 # (depth,properties)
656 levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
657
658 def complain(msg):
659         "Complain about a particular input line"
660         moan(("%s line %d: "%(file,line))+msg)
661 def moan(msg):
662         "Complain about something in general"
663         global complaints
664         print(msg);
665         if complaints is None: sys.exit(1)
666         complaints=complaints+1
667
668 class UntaintedRoot():
669         def __init__(self,s): self._s=s
670         def name(self): return self._s
671         def keyword(self): return self._s
672
673 root=level([UntaintedRoot(x) for x in ['root','root']])
674 # All vpns are children of this node
675 obstack=[root]
676 allow_defs=0   # Level above which new definitions are permitted
677
678 def set_property(obj,w):
679         "Set a property on a configuration node"
680         prop=w[0]
681         propname=prop.raw_mark_ok()
682         kw=keywords[propname]
683         if len(kw) >= 3: propname=kw[2] # for aliases
684         if propname in obj.properties:
685                 obj.properties[propname].add(obj,w)
686         else:
687                 obj.properties[propname]=kw[0](w)
688         return obj.properties[propname]
689
690 class FilterState:
691         def __init__(self):
692                 self.reset()
693         def reset(self):
694                 # called when we enter a new node,
695                 # in particular, at the start of each site
696                 pass
697
698 def pline(il,filterstate,allow_include=False):
699         "Process a configuration file line"
700         global allow_defs, obstack, root
701         w=il.rstrip('\n').split()
702         if len(w)==0: return ['']
703         w=list([Tainted(x) for x in w])
704         keyword=w[0]
705         current=obstack[len(obstack)-1]
706         copyout_core=lambda: ' '.join([ww.output() for ww in w])
707         indent='    '*len(obstack)
708         copyout=lambda: [indent + copyout_core() + '\n']
709         if keyword=='end-definitions':
710                 keyword.raw_mark_ok()
711                 allow_defs=sitelevel.depth
712                 obstack=[root]
713                 return copyout()
714         if keyword=='include':
715                 if not allow_include:
716                         complain("include not permitted here")
717                         return []
718                 if len(w) != 2:
719                         complain("include requires one argument")
720                         return []
721                 newfile=os.path.join(os.path.dirname(file),w[1].raw_mark_ok())
722                 # ^ user of "include" is trusted so raw_mark_ok is good
723                 return pfilepath(newfile,allow_include=allow_include)
724         if keyword.raw() in levels:
725                 # We may go up any number of levels, but only down by one
726                 newdepth=levels[keyword.raw_mark_ok()].depth
727                 currentdepth=len(obstack) # actually +1...
728                 if newdepth<=currentdepth:
729                         obstack=obstack[:newdepth]
730                 if newdepth>currentdepth:
731                         complain("May not go from level %d to level %d"%
732                                 (currentdepth-1,newdepth))
733                 # See if it's a new one (and whether that's permitted)
734                 # or an existing one
735                 current=obstack[len(obstack)-1]
736                 tname=w[1].name()
737                 if tname in current.children:
738                         # Not new
739                         current=current.children[tname]
740                         if service and group and current.depth==2:
741                                 if group!=current.group:
742                                         complain("Incorrect group!")
743                                 w[2].groupname()
744                 else:
745                         # New
746                         # Ignore depth check for now
747                         nl=levels[keyword.raw()](w)
748                         if nl.depth<allow_defs:
749                                 complain("New definitions not allowed at "
750                                         "level %d"%nl.depth)
751                                 # we risk crashing if we continue
752                                 sys.exit(1)
753                         current.children[tname]=nl
754                         current=nl
755                 filterstate.reset()
756                 obstack.append(current)
757                 return copyout()
758         if keyword.raw() not in current.allow_properties:
759                 complain("Property %s not allowed at %s level"%
760                         (keyword.raw(),current.type))
761                 return []
762         elif current.depth == vpnlevel.depth < allow_defs:
763                 complain("Not allowed to set VPN properties here")
764                 return []
765         else:
766                 prop=set_property(current,w)
767                 out=[copyout_core()]
768                 out=prop.forsites(output_version,out,filterstate)
769                 if len(out)==0: return [indent + '#', copyout_core(), '\n']
770                 return [indent + ' '.join(out) + '\n']
771
772         complain("unknown keyword '%s'"%(keyword.raw()))
773
774 def pfilepath(pathname,allow_include=False):
775         f=open(pathname)
776         outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
777         f.close()
778         return outlines
779
780 def pfile(name,lines,allow_include=False):
781         "Process a file"
782         global file,line
783         file=name
784         line=0
785         outlines=[]
786         filterstate = FilterState()
787         for i in lines:
788                 line=line+1
789                 if (i[0]=='#'): continue
790                 outlines += pline(i,filterstate,allow_include=allow_include)
791         return outlines
792
793 def outputsites(w):
794         "Output include file for secnet configuration"
795         w.write("# secnet sites file autogenerated by make-secnet-sites "
796                 +"version %s\n"%VERSION)
797         w.write("# %s\n"%time.asctime(time.localtime(time.time())))
798         w.write("# Command line: %s\n\n"%' '.join(sys.argv))
799
800         # Raw VPN data section of file
801         w.write(prefix+"vpn-data {\n")
802         for i in root.children.values():
803                 i.output_data(w,(i,))
804         w.write("};\n")
805
806         # Per-VPN flattened lists
807         w.write(prefix+"vpn {\n")
808         for i in root.children.values():
809                 i.output_vpnflat(w,())
810         w.write("};\n")
811
812         # Flattened list of sites
813         w.write(prefix+"all-sites %s;\n"%",".join(
814                 map(lambda x:"%svpn/%s/all-sites"%(prefix,x.kname()),
815                         root.children.values())))
816
817 line=0
818 file=None
819 complaints=0
820
821 # Sanity check section
822 # Delete nodes where leaf=0 that have no children
823
824 def live(n):
825         "Number of leafnodes below node n"
826         if n.leaf: return 1
827         for i in n.children.keys():
828                 if live(n.children[i]): return 1
829         return 0
830 def delempty(n):
831         "Delete nodes that have no leafnode children"
832         for i in list(n.children.keys()):
833                 delempty(n.children[i])
834                 if not live(n.children[i]):
835                         del n.children[i]
836
837 # Check that all constraints are met (as far as I can tell
838 # restrict-nets/networks/peer are the only special cases)
839
840 def checkconstraints(n,p,ra):
841         new_p=p.copy()
842         new_p.update(n.properties)
843         for i in n.require_properties.keys():
844                 if i not in new_p:
845                         moan("%s %s is missing property %s"%
846                                 (n.type,n.name,i))
847         for i in new_p.keys():
848                 if i not in n.allow_properties:
849                         moan("%s %s has forbidden property %s"%
850                                 (n.type,n.name,i))
851         # Check address range restrictions
852         if "restrict-nets" in n.properties:
853                 new_ra=ra.intersection(n.properties["restrict-nets"].set)
854         else:
855                 new_ra=ra
856         if "networks" in n.properties:
857                 if not n.properties["networks"].set <= new_ra:
858                         moan("%s %s networks out of bounds"%(n.type,n.name))
859                 if "peer" in n.properties:
860                         if not n.properties["networks"].set.contains(
861                                 n.properties["peer"].addr):
862                                 moan("%s %s peer not in networks"%(n.type,n.name))
863         for i in n.children.keys():
864                 checkconstraints(n.children[i],new_p,new_ra)
865
866 if service:
867         headerinput=pfilepath(header,allow_include=True)
868         userinput=sys.stdin.readlines()
869         pfile("user input",userinput)
870 else:
871         if inputfile is None:
872                 pfile("stdin",sys.stdin.readlines())
873         else:
874                 pfilepath(inputfile)
875
876 delempty(root)
877 checkconstraints(root,{},ipaddrset.complete_set())
878
879 if complaints>0:
880         if complaints==1: print("There was 1 problem.")
881         else: print("There were %d problems."%(complaints))
882         sys.exit(1)
883 complaints=None # arranges to crash if we complain later
884
885 if service:
886         # Put the user's input into their group file, and rebuild the main
887         # sites file
888         f=open(groupfiledir+"/T"+group.groupname(),'w')
889         f.write("# Section submitted by user %s, %s\n"%
890                 (user,time.asctime(time.localtime(time.time()))))
891         f.write("# Checked by make-secnet-sites version %s\n\n"%VERSION)
892         for i in userinput: f.write(i)
893         f.write("\n")
894         f.close()
895         os.rename(groupfiledir+"/T"+group.groupname(),
896                   groupfiledir+"/R"+group.groupname())
897         f=open(sitesfile+"-tmp",'w')
898         f.write("# sites file autogenerated by make-secnet-sites\n")
899         f.write("# generated %s, invoked by %s\n"%
900                 (time.asctime(time.localtime(time.time())),user))
901         f.write("# use make-secnet-sites to turn this file into a\n")
902         f.write("# valid /etc/secnet/sites.conf file\n\n")
903         for i in headerinput: f.write(i)
904         files=os.listdir(groupfiledir)
905         for i in files:
906                 if i[0]=='R':
907                         j=open(groupfiledir+"/"+i)
908                         f.write(j.read())
909                         j.close()
910         f.write("# end of sites file\n")
911         f.close()
912         os.rename(sitesfile+"-tmp",sitesfile)
913 else:
914         if outputfile is None:
915                 of=sys.stdout
916         else:
917                 tmp_outputfile=outputfile+'~tmp~'
918                 of=open(tmp_outputfile,'w')
919         outputsites(of)
920         if outputfile is not None:
921                 os.rename(tmp_outputfile,outputfile)