1#!/usr/bin/env python
2# Copyright (c) 2007, Secure64 Software Corporation
3#
4# Permission is hereby granted, free of charge, to any person obtaining a copy
5# of this software and associated documentation files (the "Software"), to deal
6# in the Software without restriction, including without limitation the rights
7# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8# copies of the Software, and to permit persons to whom the Software is
9# furnished to do so, subject to the following conditions:
10#
11# The above copyright notice and this permission notice shall be included in
12# all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20# THE SOFTWARE.
21#
22#
23#
24#	class to represent a nsd.conf file
25#
26#
27
28import os.path
29import sys
30
31if os.path.exists('../bind2nsd/Config.py'):
32   sys.path.append('../bind2nsd')
33   from Utils import *
34else:
35   from bind2nsd.Utils import *
36
37
38class NsdKey:
39
40   def __init__(self, name):
41      self.name = name
42      self.algorithm = ''
43      self.secret = ''
44      return
45
46   def dump(self, ofile):
47      if ofile == sys.stdout:
48         print >> ofile, '=> NsdKey:'
49         print >> ofile,'   name      = %s' % (self.name)
50         print >> ofile,'   algorithm = %s' % (self.algorithm)
51         print >> ofile,'   secret    = %s' % (self.secret)
52      else:
53         print >> ofile, 'key:'
54         print >> ofile,'   name: "%s"' % (self.name)
55         print >> ofile,'   algorithm: %s' % (self.algorithm)
56         print >> ofile,'   secret: "%s"' % (self.secret)
57      return
58
59   def setName(self, val):
60      self.name = val
61      return
62
63   def getName(self):
64      return self.name
65
66   def setAlgorithm(self, val):
67      self.algorithm = val
68      return
69
70   def getAlgorithm(self):
71      return self.algorithm
72
73   def setSecret(self, val):
74      self.secret = val
75      return
76
77   def getSecret(self):
78      return self.secret
79
80class NsdZone:
81
82   def __init__(self, name, config, ipkeymap):
83      self.name = name
84      self.config = config
85      self.include = ''
86      self.zonefile = ''
87      self.type = ''
88      self.allow_notify = []		# empty unless we're a slave zone
89      self.also_notify = []		# empty unless we're a master zone
90      self.provide_xfr = []
91      self.oldrootdir = ''
92      self.ipkeymap = ipkeymap
93      return
94
95   def dump(self, ofile):
96      if ofile == sys.stdout:
97         print >> ofile, '=> NsdZone:'
98         print >> ofile, '   name = %s' % (self.name)
99         print >> ofile, '   include = %s' % (self.include)
100         print >> ofile, '   zonefile = %s' % (self.zonefile)
101         print >> ofile, '   type = %s' % (self.type)
102         print >> ofile, '   allow_notify = %s' % (str(self.allow_notify))
103         print >> ofile, '   also_notify = %s' % (str(self.also_notify))
104         print >> ofile, '   provide_xfr = %s' % (str(self.provide_xfr))
105      else:
106         print >> ofile, ''
107         print >> ofile, 'zone:'
108         print >> ofile, '   name:        "%s"' % (self.name)
109         print >> ofile, '   zonefile:    "%s"' % (self.zonefile)
110
111	 # BOZO: this is a hack to avoid errors in the zone compiler
112	 # and needs to be handled more intelligently; this allows the
113	 # zone file to be non-existent (done by some ISPs) but still
114	 # listed in the named.conf
115	 if not os.path.exists(self.oldrootdir + self.zonefile):
116	    report_error('? missing zone file "%s"' % \
117	                 (self.oldrootdir + self.zonefile))
118
119         nlist = self.allow_notify
120	 if self.type == 'slave':
121	    print >> ofile, '   # this is a slave zone.  masters are listed next.'
122	    for ii in nlist:
123	       print >> ofile, '   allow-notify: %s NOKEY' % (ii)
124	       print >> ofile, '   request-xfr:  %s NOKEY' % (ii)
125	 else:
126            print >> ofile, '   include:     "%s"' % (self.include)
127            for ii in self.also_notify:
128	       print >> ofile, '   notify: %s NOKEY' % (ii)
129
130	 for ii in self.provide_xfr:
131	    if ii in self.ipkeymap:
132	       print >> ofile, '   provide-xfr: %s %s' % (ii, self.ipkeymap[ii])
133	       print >> ofile, '   notify: %s %s' % (ii, self.ipkeymap[ii])
134	    else:
135	       print >> ofile, '   provide-xfr: %s NOKEY' % (ii)
136	       print >> ofile, '   notify: %s NOKEY' % (ii)
137
138      return
139
140   def setName(self, name):
141      self.name = name
142      return
143
144   def getName(self):
145      return self.name
146
147   def setOldRootDir(self, name):
148      self.oldrootdir = name
149      return
150
151   def getOldRootDir(self):
152      return self.oldrootdir
153
154   def setZonefile(self, file):
155      self.zonefile = file
156      return
157
158   def getZonefile(self):
159      return self.zonefile
160
161   def setType(self, type):
162      self.type = type
163      return
164
165   def getType(self):
166      return self.type
167
168   def addMaster(self, quad):
169      self.masters.append(quad)
170      return
171
172   def getMasters(self):
173      return self.masters
174
175   def setInclude(self, name):
176      self.include = name
177      return
178
179   def getInclude(self):
180      return self.include
181
182   def setAllowNotify(self, quad_list):
183      self.allow_notify = quad_list
184      return
185
186   def getAllowNotify(self):
187      return self.allow_notify
188
189   def addAllowNotify(self, nlist):
190      self.allow_notify.append(nlist)
191      return
192
193   def setAlsoNotify(self, quad_list):
194      self.also_notify = quad_list
195      return
196
197   def getAlsoNotify(self):
198      return self.also_notify
199
200   def addAlsoNotify(self, nlist):
201      self.also_notify.append(nlist)
202      return
203
204   def setProvideXfr(self, quad_list):
205      self.provide_xfr = quad_list
206      return
207
208   def getProvideXfr(self):
209      return self.provide_xfr
210
211class NsdOptions:
212   def __init__(self, config):
213      self.database            = config.getValue('database')
214      self.difffile            = config.getValue('difffile')
215      self.identity            = config.getValue('identity')
216      self.ip_address          = config.getValue('ip-address')
217      self.logfile             = config.getValue('logfile')
218      self.pidfile             = config.getValue('pidfile')
219      self.port                = config.getValue('port')
220      self.statistics          = config.getValue('statistics')
221      self.username            = config.getValue('username')
222      self.xfrd_reload_timeout = config.getValue('xfrd-reload-timeout')
223      return
224
225   def dump(self, ofile):
226      if ofile == sys.stdout:
227         print >> ofile, '=> Options:'
228         print >> ofile, '   database:            %s' % (self.database)
229         print >> ofile, '   difffile:            %s' % (self.difffile)
230         print >> ofile, '   identity:            %s' % (self.identity)
231         print >> ofile, '   ip-address:          %s' % (self.ip_address)
232         print >> ofile, '   logfile:             %s' % (self.logfile)
233         print >> ofile, '   pidfile:             %s' % (self.pidfile)
234         print >> ofile, '   port:                %s' % (self.port)
235         print >> ofile, '   statistics:          %s' % (self.statistics)
236         print >> ofile, '   username:            %s' % (self.username)
237         print >> ofile, '   xfrd-reload-timeout: %s' % (self.xfrd_reload_timeout)
238      else:
239         print >> ofile, '#'
240         print >> ofile, '# All of the values below have been pulled from'
241         print >> ofile, '# either defaults or the config file'
242         print >> ofile, '#'
243         print >> ofile, ''
244         print >> ofile, 'server:'
245         print >> ofile, '   database: %s' % (self.database)
246         print >> ofile, '   difffile: %s' % (self.difffile)
247         print >> ofile, '   identity: %s' % (self.identity)
248	 iplist = self.ip_address.split()
249	 for ii in iplist:
250            print >> ofile, '   ip-address: %s' % (ii.strip())
251         print >> ofile, '   logfile: %s' % (self.logfile)
252         print >> ofile, '   pidfile: %s' % (self.pidfile)
253         print >> ofile, '   port: %s' % (self.port)
254         print >> ofile, '   statistics: %s' % (self.statistics)
255         print >> ofile, '   username: %s' % (self.username)
256         print >> ofile, '   xfrd-reload-timeout: %s' % \
257	                 (self.xfrd_reload_timeout)
258         print >> ofile, ''
259      return
260
261   def setDatabase(self, name):
262      self.database = name
263      return
264
265   def getDatabase(self):
266      return self.database
267
268   def setDifffile(self, name):
269      self.difffile = name
270      return
271
272   def getDifffile(self):
273      return self.difffile
274
275   def setIdentity(self, name):
276      self.identity = name
277      return
278
279   def getIdentity(self):
280      return self.identity
281
282   def setIpAddress(self, quad):
283      self.ip_address = quad
284      return
285
286   def getIpAddress(self):
287      return self.ip_address
288
289   def setLogfile(self, val):
290      self.logfile = val
291      return
292
293   def getLogfile(self):
294      return self.logfile
295
296   def setPort(self, val):
297      self.port = val
298      return
299
300   def getPort(self):
301      return self.port
302
303   def setPidfile(self, val):
304      self.pidfile = val
305      return
306
307   def getPidfile(self):
308      return self.pidfile
309
310   def setStatistics(self, val):
311      self.statistics = val
312      return
313
314   def getStatistics(self):
315      return self.statistics
316
317   def setXfrdReloadTimeout(self, val):
318      self.xfrd_reload_timeout = val
319      return
320
321   def getXfrdReloadTimeout(self):
322      return self.xfrd_reload_timeout
323
324class NsdConf:
325
326   def __init__(self, config):
327      self.config = config
328      self.fname = config.getValue('nsd_conf')
329      self.files = config.getValue('nsd_files')
330      self.preamble = config.getValue('nsd_preamble')
331      self.acl_list = config.getValue('acl_list')
332      self.oldrootdir = ''
333      self.options = NsdOptions(config)
334      self.includes = []
335      self.zones = {}
336      self.keys = {}
337      self.ipkeymap = {}
338      return
339
340   def populate(self, named):
341      self.oldrootdir = named.getOptions().getDirectory().replace('"','')
342      if self.oldrootdir[len(self.oldrootdir)-1] != '/':
343         self.oldrootdir += '/'
344
345      klist = named.getKeys()
346      for ii in klist:
347         oldk = named.getKey(ii)
348         k = NsdKey(oldk.getName())
349         k.setAlgorithm(oldk.getAlgorithm())
350         k.setSecret(oldk.getSecret())
351	 self.keys[k.getName()] = k
352
353	 #-- map each of the key ip addresses for faster lookup on dump()
354	 iplist = oldk.getIpAddrs()
355	 for jj in iplist:
356	    if jj not in self.ipkeymap:
357	       self.ipkeymap[jj] = k.getName()
358
359      zlist = named.getZones()
360      for ii in zlist:
361         oldz = named.getZone(ii)
362         z = NsdZone(oldz.getName(), self.config, self.ipkeymap)
363	 z.setZonefile(oldz.getFile())
364	 z.setInclude(self.acl_list)
365	 z.setOldRootDir(self.oldrootdir)
366	 if 'slave' in oldz.getType().split():
367	    z.setType('slave')		# not used, but nice to know
368	    nlist = oldz.getMasters()
369	    for ii in nlist:
370	       z.addAllowNotify(ii)
371	    if len(oldz.getAllowNotify()) > 0:
372	       nlist = oldz.getAllowNotify()
373	    else:
374	       nlist = named.getOptions().getAllowNotify()
375	    for ii in nlist:
376	       z.addAllowNotify(ii)
377	 else:
378	    z.setType('master')
379	    if len(oldz.getAlsoNotify()):
380	       nlist = oldz.getAlsoNotify()
381	    else:
382	       nlist = named.getOptions().getAlsoNotify()
383	    for ii in nlist:
384	       z.addAlsoNotify(ii)
385	 if len(named.getOptions().getAllowTransfer()) > 0:
386	    z.setProvideXfr(named.getOptions().getAllowTransfer())
387	 self.zones[z.getName()] = z
388
389      return
390
391   def dump(self):
392      report_info('=> NsdConf: dumping data from \"%s\"...' % (self.fname))
393      self.options.dump(sys.stdout)
394      report_info('=> NsdConf: list of includes...')
395      report_info( str(self.includes))
396
397      report_info('=> NsdConf: zone info...')
398      zlist = self.zones.keys()
399      zlist.sort()
400      for ii in zlist:
401         self.zones[ii].dump(sys.stdout)
402
403      report_info('=> NsdConf: key info...')
404      klist = self.keys.keys()
405      klist.sort()
406      for ii in klist():
407         self.keys[ii].dump(sys.stdout)
408
409      return
410
411   def write_conf(self):
412      nfile = open(self.fname, 'w+')
413      self.options.dump(nfile)
414
415      klist = self.keys.keys()
416      klist.sort()
417      for ii in klist:
418         self.keys[ii].dump(nfile)
419
420      zlist = self.zones.keys()
421      zlist.sort()
422      for ii in zlist:
423         report_info('   writing info for zone "%s"...' % (self.zones[ii].getName()))
424         self.zones[ii].dump(nfile)
425
426      nfile.close()
427      return
428
429   def do_generate(self, newfd, start, stop, step, lhs, rrtype, rhs):
430      #-- write the equivalent of the $GENERATE
431      for ii in range(start, stop, step):
432         left = lhs.replace('$', str(ii))
433         right = rhs.replace('$', str(ii))
434         print >> newfd, '%s %s %s' % (left, rrtype, right)
435      return
436
437   def make_zone_copy(self, oldpath, newpath):
438      #-- copy the zone file line-by-line so we can catch $GENERATE usage
439      report_info('=> copying "%s" to "%s"' % (oldpath, newpath))
440
441      if not os.path.exists(oldpath):
442         os.system('touch ' + newpath)
443	 return
444
445      oldfd = open(oldpath, 'r+')
446      newfd = open(newpath, 'w+')
447      line = oldfd.readline()
448      while line:
449         fields = line.split()
450         if len(fields) > 0 and fields[0].strip() == '$GENERATE':
451            report_info('=> processing "%s"...' % (line.strip()))
452            index = 1
453
454            #-- determine start/stop range
455            indices = fields[index].split('-')
456            start = 0
457            stop  = 0
458            if len(indices) == 2:
459               start = int(indices[0])
460               stop  = int(indices[1])
461	       if stop < start:
462                  bail('? stop < start in range "%s"' % (fields[1].strip()))
463            else:
464               bail('? invalid range "%s"' % (fields[1].strip()))
465            index += 1
466
467            #-- determine increment
468            step = 1
469            if len(fields) == 6:
470               step = int(fields[index])
471	       index += 1
472
473            #-- get lhs
474            lhs = fields[index].strip()
475            index += 1
476
477            #-- get RR type
478            rrtype = fields[index].strip()
479            if rrtype not in [ 'NS', 'PTR', 'A', 'AAAA', 'DNAME', 'CNAME' ]:
480               bail('? illegal RR type "%s"' % (rrtype))
481            index += 1
482
483            #-- get rhs
484            rhs = fields[index].strip()
485            index += 1
486
487	    #-- do the $GENERATE
488            self.do_generate(newfd, start, stop, step, lhs, rrtype, rhs)
489
490         else:		#-- just copy the line as is
491	    print >> newfd, line,
492
493         line = oldfd.readline()
494
495      oldfd.close()
496      newfd.close()
497
498      return
499
500   def write_zone_files(self):
501      acls = {}
502
503      zlist = self.zones.keys()
504      zlist.sort()
505      for ii in zlist:
506         oldpath = self.oldrootdir + self.zones[ii].getZonefile()
507	 newpath = self.config.getValue('tmpdir') + self.zones[ii].getZonefile()
508	 if not os.path.exists(os.path.dirname(newpath)):
509	    os.makedirs(os.path.dirname(newpath))
510	 self.make_zone_copy(oldpath, newpath)
511
512	 incl = self.zones[ii].getInclude()
513         if acls.has_key(incl):
514	    acls[incl] += 1
515	 else:
516	    acls[incl] = 1
517
518      alist = acls.keys()
519      alist.sort()
520      for ii in alist:
521	 newpath = self.config.getValue('tmpdir') + ii
522         run_cmd('touch ' + newpath, 'touch "%s"' % (newpath))
523
524      return
525
526   def addZone(self, zone):
527      name = zone.getName()
528      self.zones[name] = zone
529      return
530
531   def getZones(self):
532      return self.zones
533
534   def addKey(self, key):
535      self.keys[key.getName()] = key
536      return
537
538   def getKeys(self):
539      return self.keys
540
541   def getKey(self, name):
542      if name in self.keys:
543         return self.keys[name]
544      else:
545         return None
546
547