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