#!/usr/bin/python2.4 """ Python CGI script to update the DNS databases via NSUpdate """ #Copyright (c) 2006 Erich Schubert erich@debian.org #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in #all copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import os, sys, cgi, re, random, string, subprocess import kid # enable debugging import cgitb; cgitb.enable() #### CONFIGURATION zones = [ "0.100.10.in-addr.arpa" ] ns = "10.0.0.1" nskey="Kdhcpdkey" class Digger: def __init__(self): """ Constructor, generate some RE objects """ self.zonecheck = re.compile(r"^[a-zA-Z0-9_.-]+$") self.filter = re.compile(r"^([^\s]+)\s+(\d+)\s+IN\s+(PTR|A|NS|MX|SOA|SRV|CNAME|TXT)\s+([^\s]*)$",re.M) def run(self, cmd): """ Execute a dig command. """ p = subprocess.Popen(cmd, stdout=subprocess.PIPE) return p.communicate()[0] def stripZone(self, data, zone): """ Strip the zone itself (likely to be tailed with a trailing dot) from the entry, for more compact display. """ zone = ".%s" % zone l = len(zone) if data[-l:] == zone: return data[:-l] if data[-l-1:-1] == zone: return data[:-l-1] return data def getZone(self, zone): """ Load a specific zone. """ if not self.zonecheck.match(zone): raise Exception, "Zone does not match pattern" # fetch the zone out = self.run(["dig", "+nostats", "+nocmd", "-t", "AXFR", zone, "@%s" % ns]) results = self.filter.finditer(out) list = [] for match in results: e = { "name": self.stripZone(match.group(1), zone), "time": match.group(2), "type": match.group(3), "data": match.group(4), "maydel": (match.group(3) != "SOA") } list.append(e) def sorter(x,y): """ Sort IPs by integer when possible... Should be a real sort-backwards thing... """ try: return int(x["name"])-int(y["name"]) except ValueError: if x["name"][-1] == ".": if y["name"][-1] != ".": return -1 elif y["name"][-1] == ".": return +1 return cmp(x["name"],y["name"]) list.sort(sorter) return list def addRecord(self, zone, name, time, type, data): """ Add a new record to DNS """ # all kinds of safety checks. if not self.zonecheck.match(zone): raise Exception, "Zone does not match pattern." if not self.zonecheck.match(name): raise Exception, "Name does not match pattern." if not int(time): raise Excpetion, "Time value not ok." time = int(time) if not type in ["A", "TXT", "PTR", "CNAME", "NS", "MX", "SRV"]: raise Exception, "Type not allowed." if name[-1]==".": if name[-len(zone)-1:-1] != zone: raise Exception, "Name ends in a dot, "+\ "but is from a different zone!" if ('\\' in data) or ('\n' in data): raise Exception, "Double quotes, newlines and "+\ " backslashes are not allowed in data part for now!" name = self.stripZone(name, zone) # run the nsupdate command cmd = ["nsupdate", "-k", nskey] p = subprocess.Popen(cmd, close_fds=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) p.stdin.write("server %s\n" % ns) p.stdin.write("zone %s\n" % zone) p.stdin.write("update add %s.%s. %d %s %s\n" % (name, zone, time, type, data)) p.stdin.write("send\n\n") result = p.communicate()[0] if result and result != "": sys.stderr.write("%s\n" % result) return result def delRecord(self, zone, name, type): """ Delete a new record to DNS """ # all kinds of safety checks. if not self.zonecheck.match(zone): raise Exception, "Zone does not match pattern." if not self.zonecheck.match(name): raise Exception, "Name does not match pattern." if not type in ["A", "TXT", "PTR", "CNAME", "NS", "MX", "SRV"]: raise Exception, "Type not allowed." if name[-1]==".": if name[-len(zone)-1:-1] != zone: raise Exception, "Name ends in a dot, "+\ "but is from a different zone!" name = self.stripZone(name, zone) # run the nsupdate command cmd = ["nsupdate", "-k", nskey] p = subprocess.Popen(cmd, close_fds=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) p.stdin.write("server %s\n" % ns) p.stdin.write("zone %s\n" % zone) p.stdin.write("update delete %s.%s. %s\n" % (name, zone, type)) p.stdin.write("send\n\n") result = p.communicate()[0] if result and result != "": sys.stderr.write("%s\n" % result) return result # get CGI variables form = cgi.FieldStorage() # load template tpl = kid.Template(file="template.kid") zones.sort() tpl.zones = zones tpl.result = None digger = Digger() if form.has_key("zone"): zone = form["zone"].value if zone not in zones: raise Exception, "Unknown zone specified!" if form.has_key("del"): tpl.result = digger.delRecord(zone, form.getvalue("name",None), form.getvalue("type",None) ) if form.has_key("add"): tpl.result = digger.addRecord(zone, form.getvalue("name",None), form.getvalue("time",None), form.getvalue("type",None), form.getvalue("data",None) ) zonedata = digger.getZone(zone) tpl.zone = zone tpl.zonedata = zonedata else: tpl.zone = None # output result print "Content-Type: text/html\n\n" print tpl.serialize()