chiark / gitweb /
make-secnet-sites: pline: Break up `copyout'
[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
74 from argparseactionnoyes import ActionNoYes
75
76 VERSION="0.1.18"
77
78 from sys import version_info
79 if version_info.major == 2:  # for python2
80     import codecs
81     sys.stdin = codecs.getreader('utf-8')(sys.stdin)
82     sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
83     import io
84     open=lambda f,m='r': io.open(f,m,encoding='utf-8')
85
86 max={'rsa_bits':8200,'name':33,'dh_bits':8200}
87
88 def debugrepr(*args):
89         if debug_level > 0:
90                 print(repr(args), file=sys.stderr)
91
92 class Tainted:
93         def __init__(self,s,tline=None,tfile=None):
94                 self._s=s
95                 self._ok=None
96                 self._line=line if tline is None else tline
97                 self._file=file if tfile is None else tfile
98         def __eq__(self,e):
99                 return self._s==e
100         def __ne__(self,e):
101                 # for Python2
102                 return not self.__eq__(e)
103         def __str__(self):
104                 raise RuntimeError('direct use of Tainted value')
105         def __repr__(self):
106                 return 'Tainted(%s)' % repr(self._s)
107
108         def _bad(self,what,why):
109                 assert(self._ok is not True)
110                 self._ok=False
111                 complain('bad parameter: %s: %s' % (what, why))
112                 return False
113
114         def _max_ok(self,what,maxlen):
115                 if len(self._s) > maxlen:
116                         return self._bad(what,'too long (max %d)' % maxlen)
117                 return True
118
119         def _re_ok(self,bad,what,maxlen=None):
120                 if maxlen is None: maxlen=max[what]
121                 self._max_ok(what,maxlen)
122                 if self._ok is False: return False
123                 if bad.search(self._s):
124                         #print(repr(self), file=sys.stderr)
125                         return self._bad(what,'bad syntax')
126                 return True
127
128         def _rtnval(self, is_ok, ifgood, ifbad=''):
129                 if is_ok:
130                         assert(self._ok is not False)
131                         self._ok=True
132                         return ifgood
133                 else:
134                         assert(self._ok is not True)
135                         self._ok=False
136                         return ifbad
137
138         def _rtn(self, is_ok, ifbad=''):
139                 return self._rtnval(is_ok, self._s, ifbad)
140
141         def raw(self):
142                 return self._s
143         def raw_mark_ok(self):
144                 # caller promises to throw if syntax was dangeorus
145                 return self._rtn(True)
146
147         def output(self):
148                 if self._ok is False: return ''
149                 if self._ok is True: return self._s
150                 print('%s:%d: unchecked/unknown additional data "%s"' %
151                       (self._file,self._line,self._s),
152                       file=sys.stderr)
153                 sys.exit(1)
154
155         bad_name=re.compile(r'^[^a-zA-Z]|[^-_0-9a-zA-Z]')
156         # secnet accepts _ at start of names, but we reserve that
157         bad_name_counter=0
158         def name(self,what='name'):
159                 ok=self._re_ok(Tainted.bad_name,what)
160                 return self._rtn(ok,
161                                  '_line%d_%s' % (self._line, id(self)))
162
163         def keyword(self):
164                 ok=self._s in keywords or self._s in levels
165                 if not ok:
166                         complain('unknown keyword %s' % self._s)
167                 return self._rtn(ok)
168
169         bad_hex=re.compile(r'[^0-9a-fA-F]')
170         def bignum_16(self,kind,what):
171                 maxlen=(max[kind+'_bits']+3)/4
172                 ok=self._re_ok(Tainted.bad_hex,what,maxlen)
173                 return self._rtn(ok)
174
175         bad_num=re.compile(r'[^0-9]')
176         def bignum_10(self,kind,what):
177                 maxlen=math.ceil(max[kind+'_bits'] / math.log10(2))
178                 ok=self._re_ok(Tainted.bad_num,what,maxlen)
179                 return self._rtn(ok)
180
181         def number(self,minn,maxx,what='number'):
182                 # not for bignums
183                 ok=self._re_ok(Tainted.bad_num,what,10)
184                 if ok:
185                         v=int(self._s)
186                         if v<minn or v>maxx:
187                                 ok=self._bad(what,'out of range %d..%d'
188                                              % (minn,maxx))
189                 return self._rtnval(ok,v,minn)
190
191         def hexid(self,byteslen,what):
192                 ok=self._re_ok(Tainted.bad_hex,what,byteslen*2)
193                 if ok:
194                         if len(self._s) < byteslen*2:
195                                 ok=self._bad(what,'too short')
196                 return self._rtn(ok,ifbad='00'*byteslen)
197
198         bad_host=re.compile(r'[^-\][_.:0-9a-zA-Z]')
199         # We permit _ so we can refer to special non-host domains
200         # which have A and AAAA RRs.  This is a crude check and we may
201         # still produce config files with syntactically invalid
202         # domains or addresses, but that is OK.
203         def host(self):
204                 ok=self._re_ok(Tainted.bad_host,'host/address',255)
205                 return self._rtn(ok)
206
207         bad_email=re.compile(r'[^-._0-9a-z@!$%^&*=+~/]')
208         # ^ This does not accept all valid email addresses.  That's
209         # not really possible with this input syntax.  It accepts
210         # all ones that don't require quoting anywhere in email
211         # protocols (and also accepts some invalid ones).
212         def email(self):
213                 ok=self._re_ok(Tainted.bad_email,'email address',1023)
214                 return self._rtn(ok)
215
216         bad_groupname=re.compile(r'^[^_A-Za-z]|[^-+_0-9A-Za-z]')
217         def groupname(self):
218                 ok=self._re_ok(Tainted.bad_groupname,'group name',64)
219                 return self._rtn(ok)
220
221         bad_base91=re.compile(r'[^!-~]|[\'\"\\]')
222         def base91(self,what='base91'):
223                 ok=self._re_ok(Tainted.bad_base91,what,4096)
224                 return self._rtn(ok)
225
226 def parse_args():
227         global service
228         global inputfile
229         global header
230         global groupfiledir
231         global sitesfile
232         global outputfile
233         global group
234         global user
235         global of
236         global prefix
237         global key_prefix
238         global debug_level
239
240         ap = argparse.ArgumentParser(description='process secnet sites files')
241         ap.add_argument('--userv', '-u', action='store_true',
242                         help='userv service fragment update mode')
243         ap.add_argument('--conf-key-prefix', action=ActionNoYes,
244                         default=True,
245                  help='prefix conf file key names derived from sites data')
246         ap.add_argument('--prefix', '-P', nargs=1,
247                         help='set prefix')
248         ap.add_argument('--debug', '-D', action='count', default=0)
249         ap.add_argument('arg',nargs=argparse.REMAINDER)
250         av = ap.parse_args()
251         debug_level = av.debug
252         debugrepr('av',av)
253         service = 1 if av.userv else 0
254         prefix = '' if av.prefix is None else av.prefix[0]
255         key_prefix = av.conf_key_prefix
256         if service:
257                 if len(av.arg)!=4:
258                         print("Wrong number of arguments")
259                         sys.exit(1)
260                 (header, groupfiledir, sitesfile, group) = av.arg
261                 group = Tainted(group,0,'command line')
262                 # untrusted argument from caller
263                 if "USERV_USER" not in os.environ:
264                         print("Environment variable USERV_USER not found")
265                         sys.exit(1)
266                 user=os.environ["USERV_USER"]
267                 # Check that group is in USERV_GROUP
268                 if "USERV_GROUP" not in os.environ:
269                         print("Environment variable USERV_GROUP not found")
270                         sys.exit(1)
271                 ugs=os.environ["USERV_GROUP"]
272                 ok=0
273                 for i in ugs.split():
274                         if group==i: ok=1
275                 if not ok:
276                         print("caller not in group %s"%group)
277                         sys.exit(1)
278         else:
279                 if len(av.arg)>3:
280                         print("Too many arguments")
281                         sys.exit(1)
282                 (inputfile, outputfile) = (av.arg + [None]*2)[0:2]
283
284 parse_args()
285
286 # Classes describing possible datatypes in the configuration file
287
288 class basetype:
289         "Common protocol for configuration types."
290         def add(self,obj,w):
291                 complain("%s %s already has property %s defined"%
292                         (obj.type,obj.name,w[0].raw()))
293
294 class conflist:
295         "A list of some kind of configuration type."
296         def __init__(self,subtype,w):
297                 self.subtype=subtype
298                 self.list=[subtype(w)]
299         def add(self,obj,w):
300                 self.list.append(self.subtype(w))
301         def __str__(self):
302                 return ', '.join(map(str, self.list))
303 def listof(subtype):
304         return lambda w: conflist(subtype, w)
305
306 class single_ipaddr (basetype):
307         "An IP address"
308         def __init__(self,w):
309                 self.addr=ipaddress.ip_address(w[1].raw_mark_ok())
310         def __str__(self):
311                 return '"%s"'%self.addr
312
313 class networks (basetype):
314         "A set of IP addresses specified as a list of networks"
315         def __init__(self,w):
316                 self.set=ipaddrset.IPAddressSet()
317                 for i in w[1:]:
318                         x=ipaddress.ip_network(i.raw_mark_ok(),strict=True)
319                         self.set.append([x])
320         def __str__(self):
321                 return ",".join(map((lambda n: '"%s"'%n), self.set.networks()))
322
323 class dhgroup (basetype):
324         "A Diffie-Hellman group"
325         def __init__(self,w):
326                 self.mod=w[1].bignum_16('dh','dh mod')
327                 self.gen=w[2].bignum_16('dh','dh gen')
328         def __str__(self):
329                 return 'diffie-hellman("%s","%s")'%(self.mod,self.gen)
330
331 class hash (basetype):
332         "A choice of hash function"
333         def __init__(self,w):
334                 hname=w[1]
335                 self.ht=hname.raw()
336                 if (self.ht!='md5' and self.ht!='sha1'):
337                         complain("unknown hash type %s"%(self.ht))
338                         self.ht=None
339                 else:
340                         hname.raw_mark_ok()
341         def __str__(self):
342                 return '%s'%(self.ht)
343
344 class email (basetype):
345         "An email address"
346         def __init__(self,w):
347                 self.addr=w[1].email()
348         def __str__(self):
349                 return '<%s>'%(self.addr)
350
351 class boolean (basetype):
352         "A boolean"
353         def __init__(self,w):
354                 v=w[1]
355                 if re.match('[TtYy1]',v.raw()):
356                         self.b=True
357                         v.raw_mark_ok()
358                 elif re.match('[FfNn0]',v.raw()):
359                         self.b=False
360                         v.raw_mark_ok()
361                 else:
362                         complain("invalid boolean value");
363         def __str__(self):
364                 return ['False','True'][self.b]
365
366 class num (basetype):
367         "A decimal number"
368         def __init__(self,w):
369                 self.n=w[1].number(0,0x7fffffff)
370         def __str__(self):
371                 return '%d'%(self.n)
372
373 class address (basetype):
374         "A DNS name and UDP port number"
375         def __init__(self,w):
376                 self.adr=w[1].host()
377                 self.port=w[2].number(1,65536,'port')
378         def __str__(self):
379                 return '"%s"; port %d'%(self.adr,self.port)
380
381 class rsakey (basetype):
382         "An RSA public key"
383         def __init__(self,w):
384                 self.l=w[1].number(0,max['rsa_bits'],'rsa len')
385                 self.e=w[2].bignum_10('rsa','rsa e')
386                 self.n=w[3].bignum_10('rsa','rsa n')
387                 if len(w) >= 5: w[4].email()
388         def __str__(self):
389                 return 'rsa-public("%s","%s")'%(self.e,self.n)
390
391 # Possible properties of configuration nodes
392 keywords={
393  'contact':(email,"Contact address"),
394  'dh':(dhgroup,"Diffie-Hellman group"),
395  'hash':(hash,"Hash function"),
396  'key-lifetime':(num,"Maximum key lifetime (ms)"),
397  'setup-timeout':(num,"Key setup timeout (ms)"),
398  'setup-retries':(num,"Maximum key setup packet retries"),
399  'wait-time':(num,"Time to wait after unsuccessful key setup (ms)"),
400  'renegotiate-time':(num,"Time after key setup to begin renegotiation (ms)"),
401  'restrict-nets':(networks,"Allowable networks"),
402  'networks':(networks,"Claimed networks"),
403  'pubkey':(listof(rsakey),"RSA public site key"),
404  'peer':(single_ipaddr,"Tunnel peer IP address"),
405  'address':(address,"External contact address and port"),
406  'mobile':(boolean,"Site is mobile"),
407 }
408
409 def sp(name,value):
410         "Simply output a property - the default case"
411         return "%s %s;\n"%(name,value)
412
413 # All levels support these properties
414 global_properties={
415         'contact':(lambda name,value:"# Contact email address: %s\n"%(value)),
416         'dh':sp,
417         'hash':sp,
418         'key-lifetime':sp,
419         'setup-timeout':sp,
420         'setup-retries':sp,
421         'wait-time':sp,
422         'renegotiate-time':sp,
423         'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value),
424 }
425
426 class level:
427         "A level in the configuration hierarchy"
428         depth=0
429         leaf=0
430         allow_properties={}
431         require_properties={}
432         def __init__(self,w):
433                 self.type=w[0].keyword()
434                 self.name=w[1].name()
435                 self.properties={}
436                 self.children={}
437         def indent(self,w,t):
438                 w.write("                 "[:t])
439         def prop_out(self,n):
440                 return self.allow_properties[n](n,str(self.properties[n]))
441         def output_props(self,w,ind):
442                 for i in sorted(self.properties.keys()):
443                         if self.allow_properties[i]:
444                                 self.indent(w,ind)
445                                 w.write("%s"%self.prop_out(i))
446         def kname(self):
447                 return ((self.type[0].upper() if key_prefix else '')
448                         + self.name)
449         def output_data(self,w,path):
450                 ind = 2*len(path)
451                 self.indent(w,ind)
452                 w.write("%s {\n"%(self.kname()))
453                 self.output_props(w,ind+2)
454                 if self.depth==1: w.write("\n");
455                 for k in sorted(self.children.keys()):
456                         c=self.children[k]
457                         c.output_data(w,path+(c,))
458                 self.indent(w,ind)
459                 w.write("};\n")
460
461 class vpnlevel(level):
462         "VPN level in the configuration hierarchy"
463         depth=1
464         leaf=0
465         type="vpn"
466         allow_properties=global_properties.copy()
467         require_properties={
468          'contact':"VPN admin contact address"
469         }
470         def __init__(self,w):
471                 level.__init__(self,w)
472         def output_vpnflat(self,w,path):
473                 "Output flattened list of site names for this VPN"
474                 ind=2*(len(path)+1)
475                 self.indent(w,ind)
476                 w.write("%s {\n"%(self.kname()))
477                 for i in self.children.keys():
478                         self.children[i].output_vpnflat(w,path+(self,))
479                 w.write("\n")
480                 self.indent(w,ind+2)
481                 w.write("all-sites %s;\n"%
482                         ','.join(map(lambda i: i.kname(),
483                                      self.children.values())))
484                 self.indent(w,ind)
485                 w.write("};\n")
486
487 class locationlevel(level):
488         "Location level in the configuration hierarchy"
489         depth=2
490         leaf=0
491         type="location"
492         allow_properties=global_properties.copy()
493         require_properties={
494          'contact':"Location admin contact address",
495         }
496         def __init__(self,w):
497                 level.__init__(self,w)
498                 self.group=w[2].groupname()
499         def output_vpnflat(self,w,path):
500                 ind=2*(len(path)+1)
501                 self.indent(w,ind)
502                 # The "path=path,self=self" abomination below exists because
503                 # Python didn't support nested_scopes until version 2.1
504                 #
505                 #"/"+self.name+"/"+i
506                 w.write("%s %s;\n"%(self.kname(),','.join(
507                         map(lambda x,path=path,self=self:
508                             '/'.join([prefix+"vpn-data"] + list(map(
509                                     lambda i: i.kname(),
510                                     path+(self,x)))),
511                             self.children.values()))))
512
513 class sitelevel(level):
514         "Site level (i.e. a leafnode) in the configuration hierarchy"
515         depth=3
516         leaf=1
517         type="site"
518         allow_properties=global_properties.copy()
519         allow_properties.update({
520          'address':sp,
521          'networks':None,
522          'peer':None,
523          'pubkey':None,
524          'mobile':sp,
525         })
526         require_properties={
527          'dh':"Diffie-Hellman group",
528          'contact':"Site admin contact address",
529          'networks':"Networks claimed by the site",
530          'hash':"hash function",
531          'peer':"Gateway address of the site",
532          'pubkey':"RSA public key of the site",
533         }
534         def __init__(self,w):
535                 level.__init__(self,w)
536         def output_data(self,w,path):
537                 ind=2*len(path)
538                 np='/'.join(map(lambda i: i.name, path))
539                 self.indent(w,ind)
540                 w.write("%s {\n"%(self.kname()))
541                 self.indent(w,ind+2)
542                 w.write("name \"%s\";\n"%(np,))
543                 self.indent(w,ind+2)
544                 w.write("key %s;\n"%str(self.properties["pubkey"].list[0]))
545                 self.output_props(w,ind+2)
546                 self.indent(w,ind+2)
547                 w.write("link netlink {\n");
548                 self.indent(w,ind+4)
549                 w.write("routes %s;\n"%str(self.properties["networks"]))
550                 self.indent(w,ind+4)
551                 w.write("ptp-address %s;\n"%str(self.properties["peer"]))
552                 self.indent(w,ind+2)
553                 w.write("};\n")
554                 self.indent(w,ind)
555                 w.write("};\n")
556
557 # Levels in the configuration file
558 # (depth,properties)
559 levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
560
561 def complain(msg):
562         "Complain about a particular input line"
563         moan(("%s line %d: "%(file,line))+msg)
564 def moan(msg):
565         "Complain about something in general"
566         global complaints
567         print(msg);
568         if complaints is None: sys.exit(1)
569         complaints=complaints+1
570
571 class UntaintedRoot():
572         def __init__(self,s): self._s=s
573         def name(self): return self._s
574         def keyword(self): return self._s
575
576 root=level([UntaintedRoot(x) for x in ['root','root']])
577 # All vpns are children of this node
578 obstack=[root]
579 allow_defs=0   # Level above which new definitions are permitted
580
581 def set_property(obj,w):
582         "Set a property on a configuration node"
583         prop=w[0]
584         propname=prop.raw_mark_ok()
585         kw=keywords[propname]
586         if len(kw) >= 3: propname=kw[2] # for aliases
587         if propname in obj.properties:
588                 obj.properties[propname].add(obj,w)
589         else:
590                 obj.properties[propname]=kw[0](w)
591
592 class FilterState:
593         def __init__(self):
594                 self.reset()
595         def reset(self):
596                 # called when we enter a new node,
597                 # in particular, at the start of each site
598                 pass
599
600 def pline(il,filterstate,allow_include=False):
601         "Process a configuration file line"
602         global allow_defs, obstack, root
603         w=il.rstrip('\n').split()
604         if len(w)==0: return ['']
605         w=list([Tainted(x) for x in w])
606         keyword=w[0]
607         current=obstack[len(obstack)-1]
608         copyout_core=lambda: ' '.join([ww.output() for ww in w])
609         indent='    '*len(obstack)
610         copyout=lambda: [indent + copyout_core() + '\n']
611         if keyword=='end-definitions':
612                 keyword.raw_mark_ok()
613                 allow_defs=sitelevel.depth
614                 obstack=[root]
615                 return copyout()
616         if keyword=='include':
617                 if not allow_include:
618                         complain("include not permitted here")
619                         return []
620                 if len(w) != 2:
621                         complain("include requires one argument")
622                         return []
623                 newfile=os.path.join(os.path.dirname(file),w[1].raw_mark_ok())
624                 # ^ user of "include" is trusted so raw_mark_ok is good
625                 return pfilepath(newfile,allow_include=allow_include)
626         if keyword.raw() in levels:
627                 # We may go up any number of levels, but only down by one
628                 newdepth=levels[keyword.raw_mark_ok()].depth
629                 currentdepth=len(obstack) # actually +1...
630                 if newdepth<=currentdepth:
631                         obstack=obstack[:newdepth]
632                 if newdepth>currentdepth:
633                         complain("May not go from level %d to level %d"%
634                                 (currentdepth-1,newdepth))
635                 # See if it's a new one (and whether that's permitted)
636                 # or an existing one
637                 current=obstack[len(obstack)-1]
638                 tname=w[1].name()
639                 if tname in current.children:
640                         # Not new
641                         current=current.children[tname]
642                         if service and group and current.depth==2:
643                                 if group!=current.group:
644                                         complain("Incorrect group!")
645                                 w[2].groupname()
646                 else:
647                         # New
648                         # Ignore depth check for now
649                         nl=levels[keyword.raw()](w)
650                         if nl.depth<allow_defs:
651                                 complain("New definitions not allowed at "
652                                         "level %d"%nl.depth)
653                                 # we risk crashing if we continue
654                                 sys.exit(1)
655                         current.children[tname]=nl
656                         current=nl
657                 filterstate.reset()
658                 obstack.append(current)
659                 return copyout()
660         if keyword.raw() not in current.allow_properties:
661                 complain("Property %s not allowed at %s level"%
662                         (keyword.raw(),current.type))
663                 return []
664         elif current.depth == vpnlevel.depth < allow_defs:
665                 complain("Not allowed to set VPN properties here")
666                 return []
667         else:
668                 set_property(current,w)
669                 return copyout()
670
671         complain("unknown keyword '%s'"%(keyword.raw()))
672
673 def pfilepath(pathname,allow_include=False):
674         f=open(pathname)
675         outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
676         f.close()
677         return outlines
678
679 def pfile(name,lines,allow_include=False):
680         "Process a file"
681         global file,line
682         file=name
683         line=0
684         outlines=[]
685         filterstate = FilterState()
686         for i in lines:
687                 line=line+1
688                 if (i[0]=='#'): continue
689                 outlines += pline(i,filterstate,allow_include=allow_include)
690         return outlines
691
692 def outputsites(w):
693         "Output include file for secnet configuration"
694         w.write("# secnet sites file autogenerated by make-secnet-sites "
695                 +"version %s\n"%VERSION)
696         w.write("# %s\n"%time.asctime(time.localtime(time.time())))
697         w.write("# Command line: %s\n\n"%' '.join(sys.argv))
698
699         # Raw VPN data section of file
700         w.write(prefix+"vpn-data {\n")
701         for i in root.children.values():
702                 i.output_data(w,(i,))
703         w.write("};\n")
704
705         # Per-VPN flattened lists
706         w.write(prefix+"vpn {\n")
707         for i in root.children.values():
708                 i.output_vpnflat(w,())
709         w.write("};\n")
710
711         # Flattened list of sites
712         w.write(prefix+"all-sites %s;\n"%",".join(
713                 map(lambda x:"%svpn/%s/all-sites"%(prefix,x.kname()),
714                         root.children.values())))
715
716 line=0
717 file=None
718 complaints=0
719
720 # Sanity check section
721 # Delete nodes where leaf=0 that have no children
722
723 def live(n):
724         "Number of leafnodes below node n"
725         if n.leaf: return 1
726         for i in n.children.keys():
727                 if live(n.children[i]): return 1
728         return 0
729 def delempty(n):
730         "Delete nodes that have no leafnode children"
731         for i in list(n.children.keys()):
732                 delempty(n.children[i])
733                 if not live(n.children[i]):
734                         del n.children[i]
735
736 # Check that all constraints are met (as far as I can tell
737 # restrict-nets/networks/peer are the only special cases)
738
739 def checkconstraints(n,p,ra):
740         new_p=p.copy()
741         new_p.update(n.properties)
742         for i in n.require_properties.keys():
743                 if i not in new_p:
744                         moan("%s %s is missing property %s"%
745                                 (n.type,n.name,i))
746         for i in new_p.keys():
747                 if i not in n.allow_properties:
748                         moan("%s %s has forbidden property %s"%
749                                 (n.type,n.name,i))
750         # Check address range restrictions
751         if "restrict-nets" in n.properties:
752                 new_ra=ra.intersection(n.properties["restrict-nets"].set)
753         else:
754                 new_ra=ra
755         if "networks" in n.properties:
756                 if not n.properties["networks"].set <= new_ra:
757                         moan("%s %s networks out of bounds"%(n.type,n.name))
758                 if "peer" in n.properties:
759                         if not n.properties["networks"].set.contains(
760                                 n.properties["peer"].addr):
761                                 moan("%s %s peer not in networks"%(n.type,n.name))
762         for i in n.children.keys():
763                 checkconstraints(n.children[i],new_p,new_ra)
764
765 if service:
766         headerinput=pfilepath(header,allow_include=True)
767         userinput=sys.stdin.readlines()
768         pfile("user input",userinput)
769 else:
770         if inputfile is None:
771                 pfile("stdin",sys.stdin.readlines())
772         else:
773                 pfilepath(inputfile)
774
775 delempty(root)
776 checkconstraints(root,{},ipaddrset.complete_set())
777
778 if complaints>0:
779         if complaints==1: print("There was 1 problem.")
780         else: print("There were %d problems."%(complaints))
781         sys.exit(1)
782 complaints=None # arranges to crash if we complain later
783
784 if service:
785         # Put the user's input into their group file, and rebuild the main
786         # sites file
787         f=open(groupfiledir+"/T"+group.groupname(),'w')
788         f.write("# Section submitted by user %s, %s\n"%
789                 (user,time.asctime(time.localtime(time.time()))))
790         f.write("# Checked by make-secnet-sites version %s\n\n"%VERSION)
791         for i in userinput: f.write(i)
792         f.write("\n")
793         f.close()
794         os.rename(groupfiledir+"/T"+group.groupname(),
795                   groupfiledir+"/R"+group.groupname())
796         f=open(sitesfile+"-tmp",'w')
797         f.write("# sites file autogenerated by make-secnet-sites\n")
798         f.write("# generated %s, invoked by %s\n"%
799                 (time.asctime(time.localtime(time.time())),user))
800         f.write("# use make-secnet-sites to turn this file into a\n")
801         f.write("# valid /etc/secnet/sites.conf file\n\n")
802         for i in headerinput: f.write(i)
803         files=os.listdir(groupfiledir)
804         for i in files:
805                 if i[0]=='R':
806                         j=open(groupfiledir+"/"+i)
807                         f.write(j.read())
808                         j.close()
809         f.write("# end of sites file\n")
810         f.close()
811         os.rename(sitesfile+"-tmp",sitesfile)
812 else:
813         if outputfile is None:
814                 of=sys.stdout
815         else:
816                 tmp_outputfile=outputfile+'~tmp~'
817                 of=open(tmp_outputfile,'w')
818         outputsites(of)
819         if outputfile is not None:
820                 os.rename(tmp_outputfile,outputfile)