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