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