#!/usr/bin/python import base64, getopt, httplib, os, re, sys, stat, string, time # # zonecheck.py # # Copyright GNU GENERAL PUBLIC LICENSE Version 2 # http://www.gnu.org/copyleft/gpl.html # # Author : Kal # # # global constants # Version = "0.01" Dyndnshost = "dynamic.zoneedit.com" Dyndnsnic = "/auth/dynamic.html" Useragent = "zonecheck/" + Version Touchage = 25 # days after which to force an update Linuxip = "/sbin/ifconfig" # ifconfig command under linux Win32ip = "ipconfig /all" # ipconfig command under win32 # # Linksys router support details from ls_dyndns.py bgriggs@pobox.com # # leave Linksys_host = "" to autodetect via the default route # enter an ip here to skip the autodetect # Linksys_host = "" Linksys_user = "" Linksys_page = "/Status.htm" #password is specified by command line option -L # regular expression for address Addressgrep = re.compile ('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}') def Usage(): print print "Usage : zonecheck.py [options] Username Password Hostnames" print "Options: -a address manually specify the address " print " -f force update regardless of current state " print " -g NAT router, let zoneedit guess your IP " print " (do not use this in a cron job, try -r) " print " -h print the detailed help text " print " -i interface interface for local address (default ppp0) " print " -l log debugging text to zonecheck.log file " print " -m set type MX " print " -n node node in zone (ie. ftp) " print " -r URL NAT router, use web IP detection " print " -v verbose mode " print " -L password Linksys NAT router password (eg BEFSR41) " print print "Note you will have to set the -i interface option if your " print "link to the internet is not on ppp0 (the default). " print print "Hostnames can be a comma separated list (no spaces). Example: " print "python zonecheck.py username password ddd.com,ddd.org,ddd.net " print sys.exit(0) def Help(): print print "Start zonecheck.py with no arguments to get the usage screen." print print "If -f is set, all hosts will be updated regardless of the " print "current error state. You should never need this. " print print "The file zonecheck.dat contains the IP address and hostnames " print "used in the last update. If you run zonecheck from a cronjob, " print "make sure the hostnames are the same in each execution. If " print "the zonecheck.dat file is older than " + `Touchage` + " days, an update will be " print "made to touch the hostnames with zoneedit. " print print "The file zonecheck.err is created if zoneedit responds with an error. " print "zonecheck will not try to update again until this error is resolved. " print "You must remove the file yourself once the problem is fixed. " print print "If a http message is sent to zoneedit, the response will be " print "saved in the zonecheck.html file." print print "NAT Routers:" print "If you have the Linksys NAT router then simply use the -L " print "option to specify your linksys router admin password. " print "zonecheck will locate the IP of your router automatically by " print "looking at the default route of the machine you are running on. " print "(You can override this behavior by changing the Linksys_host " print "variable in the script.) Then zonecheck will read the status " print "page for the WAN IP address and use that for updates. " print print "You can use a specific web based IP detection via -r URL. " print print "Win32 Systems: " print "1) install python from www.python.org " print "2) copy the zonecheck.py file to the python directory " print "3) set the interface parameter to some text that appears " print "before the correct address in the output of ipconfig /all " print print "For example you can use the model name or the mac address: " print "python zonecheck.py -i LNE100TX username password hostnames " print "python zonecheck.py -i 4A-C9-1F-3E-A3-E7 username password hostnames " print sys.exit(0) class Logger: # # open a new log file in the target dir if logging # a race condition if there are tons of scripts # starting at the same time and should really use locking # but that would be overkill for this app # def __init__(self, logname = "zonecheck.log", verbose = 0, logging = 0): self.logname = logname self.verbose = verbose self.logging = logging self.prefix = "zonecheck.py: " if self.logging == 1: self.logfp = open(self.logname, "w") self.logfp.write(Useragent + "\n") self.logfp.write(self.prefix + "logging to " + self.logname + "\n") self.logfp.close() # normal logging message def logit(self, logline): if self.verbose: print self.prefix + logline if self.logging: self.logfp = open(self.logname, "a") self.logfp.write(self.prefix + logline + "\n") self.logfp.close() # logging message that gets printed even if not verbose def logexit(self, logline): print self.prefix + logline if self.logging: self.logfp = open(self.logname, "a") self.logfp.write(self.prefix + logline + "\n") self.logfp.close() if __name__=="__main__": # # default options # opt_address = "" opt_force = 0 opt_logging = 0 opt_verbose = 0 opt_hostnames = "" opt_interface = "ppp0" opt_username = "" opt_password = "" opt_router = "" opt_guess = 0 opt_LS_password = "" opt_mxtype = 0 opt_node = "" # # parse the command line options # if len(sys.argv) == 1: Usage() try: opts, args = getopt.getopt(sys.argv[1:], "a:hi:flr:vgL:mn:") except getopt.error, reason: print reason sys.exit(-1) # # check verbose, logging and detailed help options first # for opt in opts: (lopt, ropt) = opt if lopt == "-l": opt_logging = 1 elif lopt == "-v": opt_verbose = 1 elif lopt == "-h": Help() # # create the logger object # logger = Logger("zonecheck.log", opt_verbose, opt_logging) # # must have 3 arguments (username, password, hostnames) to continue # if len(args) != 3: Usage() # # okay now parse rest of the options and log as needed # for opt in opts: (lopt, ropt) = opt if lopt == "-a": opt_address = ropt logline = "opt_address set to " + opt_address logger.logit(logline) elif lopt == "-i": opt_interface = ropt logline = "opt_interface set to " + opt_interface logger.logit(logline) elif lopt == "-f": opt_force = 1 logline = "opt_force set " logger.logit(logline) elif lopt == "-r": opt_router = ropt logline = "opt_router set to " + opt_router logger.logit(logline) elif lopt == "-g": opt_guess = 1 logline = "opt_guess set " logger.logit(logline) elif lopt == "-L": opt_LS_password = ropt logline = "opt_LS_password = " for x in xrange(0, len(opt_LS_password)): logline = logline + "*" logger.logit(logline) elif lopt == "-m": opt_mxtype = 1 logline = "opt_mxtype set " logger.logit(logline) elif lopt == "-n": opt_node = ropt logline = "opt_node set to " + opt_node logger.logit(logline) # # store the command line arguments # opt_username = args[0] logline = "opt_username = " + opt_username logger.logit(logline) opt_password = args[1] logline = "opt_password = " for x in xrange(0, len(opt_password)): logline = logline + "*" logger.logit(logline) opt_hostnames = args[2] logline = "opt_hostnames = " + opt_hostnames logger.logit(logline) hostnames = string.split(opt_hostnames, ",") # # taint check, make sure each hostname is a dotted fqdn # for host in hostnames: if string.find(host, ".") == -1: logline = "Bad hostname: " + host logger.logexit(logline) sys.exit(-1) # # determine the local machine's ip # localip = "" if opt_address != "": logger.logit("manually setting localip") localip = opt_address elif opt_LS_password != "": # # Linksys router ip detection # ipdir = Linksys_page # # determine the linksys router host address # iphost = "" if Linksys_host != "": logger.logit("Linksys_host set explicitly.") iphost = Linksys_host else: if sys.platform == "win32": logger.logit("WIN32 route detection for Linksys router.") os.system ("route print " + " > " + "localip.out") fp = open("localip.out", "r") while 1: fileline = fp.readline() if not fileline: fp.close() break p1 = string.find(fileline, "0.0.0.0") if p1 != -1: ipmatch = Addressgrep.findall(fileline) if ipmatch != None: if len(ipmatch) > 2: iphost = ipmatch[2] else: logger.logit("Linux route detection for Linksys router.") os.system ("/sbin/route -n " + " > " + "localip.out") fp = open("localip.out", "r") while 1: fileline = fp.readline() if not fileline: fp.close() break p1 = string.find(fileline, "UG") if p1 != -1: ipmatch = Addressgrep.findall(fileline) if ipmatch != None: if len(ipmatch) > 1: iphost = ipmatch[1] if iphost == "": logger.logit("No router ip detected. Assuming 192.168.1.1") iphost = "192.168.1.1" else: logline = "Trying linksys router at " + iphost logger.logit(logline) # connect to the router's admin webpage try: h1 = httplib.HTTP(iphost) # # Hack from ls_dyndns.py for authorization. # # For some reason the router won't authenticate when # using standard headers for authorization. Like this: # #h1.putrequest('GET', ipdir) #authstring = base64.encodestring(Linksys_user + ":" + opt_LS_password) #h1.putheader("Authorization", "Basic " + authstring) # # It may be looking for lines that end with just \n not \r\n. # Anyways here is the snip from ls_dyndns.py # ipdir = '/Status.htm \n' \ + 'Authorization: Basic ' \ + base64.encodestring (' ' + ':' + opt_LS_password) + '\n\n' h1.putrequest('GET', ipdir) h1.endheaders() errcode, errmsg, headers = h1.getreply() fp = h1.getfile() ipdata = fp.read() fp.close() except: logline = "No address found on router at " + iphost logger.logexit(logline) sys.exit(-1) # create an output file of the linksys response fp = open("linksys.html", "w") fp.write(ipdata) fp.close() logger.logit("linksys.html file created") # grab everything that looks like an IP address ipmatch = Addressgrep.findall(ipdata) if ipmatch != None: if len(ipmatch) > 2: localip = ipmatch[2] if localip == "0.0.0.0": logline = "The router has no WAN IP assigned. " logger.logexit(logline) sys.exit(-1) if localip == "": logline = "No address found on router with password " + opt_LS_password logger.logexit(logline) sys.exit(-1) elif opt_router != "": logger.logit("web based ip detection for localip") ipurl = "" iphost = "" ipport = 80 ipdir = "" # strip off the http part, if any if ipurl[:7] == "HTTP://" or ipurl[:7] == "http://": ipurl = opt_router[7:] else: ipurl = opt_router # if port number is specified p1 = string.find(ipurl, ":") if p1 != -1: p2 = string.find(ipurl, "/", p1) ipport = string.atoi(ipurl[p1+1:p2]) iphost = ipurl[:p1] ipdir = ipurl[p2:] else: p1 = string.find(ipurl, "/") ipport = 80 ipdir = ipurl[p1:] iphost = ipurl[:p1] logger.logit("trying host " + iphost) logger.logit("trying port " + `ipport`) logger.logit("trying dir " + ipdir) # grab the data h1 = httplib.HTTP(iphost, ipport) h1.putrequest('GET', ipdir + ' HTTP/1.0') h1.putheader("HOST: " + iphost) h1.putheader("USER-AGENT: " + Useragent) h1.endheaders() errcode, errmsg, headers = h1.getreply() fp = h1.getfile() ipdata = fp.read() fp.close() # create an output file of the ip detection response fp = open("webip.html", "w") fp.write(ipdata) fp.close() logger.logit("webip.html file created") # grab everything that looks like an IP address ipmatch = Addressgrep.findall(ipdata) if ipmatch != None: if len(ipmatch) > 0: localip = ipmatch[0] if localip == "": logline = "No address found at " + opt_router logger.logexit(logline) sys.exit(-1) else: logger.logit("interface ip detection for localip") if sys.platform == "win32": localip = "" getip = Win32ip os.system (getip + " > " + "localip.out") fp = open("localip.out", "r") ipdata = fp.read() fp.close() p1 = string.find(ipdata, opt_interface) if p1 != -1: ipmatch = Addressgrep.search(ipdata, p1) if ipmatch != None: localip = ipmatch.group() else: getip = Linuxip + " " + opt_interface os.system (getip + " > " + "localip.out") fp = open("localip.out", "r") ipdata = fp.read() fp.close() p1 = string.find(ipdata, opt_interface) if p1 != -1: ipmatch = Addressgrep.search(ipdata, p1) if ipmatch != None: localip = ipmatch.group() if localip == "": logline = "No address found on interface " + opt_interface logger.logexit(logline) sys.exit(-1) # # check the IP to make sure it is sensible # p1 = string.find(localip, ".") p2 = string.find(localip, ".", p1+1) p3 = string.find(localip, ".", p2+1) p4 = string.find(localip, ".", p3+1) if p1 == -1 or p2 == -1 or p3 == -1 or p4 != -1: logline = "Invalid local address " + localip logger.logexit(logline) sys.exit(-1) try: ip1 = string.atoi(localip[0:p1]) ip2 = string.atoi(localip[p1+1:p2]) ip3 = string.atoi(localip[p2+1:p3]) ip4 = string.atoi(localip[p3+1:]) except: ip1 = 0 ip2 = 0 ip3 = 0 ip4 = 0 if ip1 < 0 or ip1 > 254 or ip2 < 0 or ip2 > 254 or ip3 < 0 or ip3 > 254 or ip4 < 0 or ip4 > 254: logline = "Invalid local address " + localip logger.logexit(logline) sys.exit(-1) # # read the data from file of last update, if any # fileip = "" filehosts = [] fileage = 0 try: fp = open ("zonecheck.dat", "r") fileip = fp.readline() if fileip[-1] == "\n": fileip = fileip[:-1] while 1: fileline = fp.readline() if not fileline: break filehosts.append(fileline[:-1]) fp.close() # # get the age of the file # currtime = time.time() statinfo = os.stat("zonecheck.dat") fileage = (currtime - statinfo[8]) / (60*60*24) except: logger.logit("No zonecheck.dat file.") # # read the data from error file, if any # errors = [] try: fp = open ("zonecheck.err", "r") while 1: errline = fp.readline() if not errline: break errors.append(errline[:-1]) fp.close() except: logger.logit("Good, no zonecheck.err file.") if len(errors) > 0 and opt_force == 0: logger.logexit("Aborting because of errors in zonecheck.err file.") logger.logexit("Erase the zonecheck.err file if this is fixed now.") sys.exit(-1) # # determine whether and which hosts need updating # updatehosts = [] # if opt_force is set then update all hosts if opt_force == 1: logger.logit("Updates forced by -f option.") for host in hostnames: updatehosts.append(host) # else if file age is older than update all hosts elif fileage > Touchage: logger.logit("Updates required by stale zonecheck.dat file.") for host in hostnames: updatehosts.append(host) # else check the address used in last update elif localip != fileip: logger.logit("Updates required by zonecheck.dat address mismatch.") for host in hostnames: updatehosts.append(host) # check each hostname to see if the last update was the same address else: logger.logit("Checking hosts in file vs command line.") updateflag = 0 for host in hostnames: if host not in filehosts: updateflag = 1 if updateflag == 1: for host in hostnames: updatehosts.append(host) if updatehosts == []: # Quietly log this message then exit too. logger.logit("IP data on file matches local address.") sys.exit(0) # # build the query strings # updateprefix = Dyndnsnic + "?" hostlist = "zones=" for host in updatehosts: hostlist = hostlist + host + "," logger.logit(host + " needs updating") if hostlist[-1:] == ",": hostlist = hostlist[:-1] if opt_guess == 1: logger.logit("Letting zoneedit guess the IP.") updatesuffix = "" localip = "" else: updatesuffix = "&dnsto=" + localip if opt_mxtype == 1: updatesuffix = updatesuffix + "&type=MX" if opt_node != "": updatesuffix = updatesuffix + "&dnsfrom=" + opt_node logger.logit("Prefix = " + updateprefix) logger.logit("Hosts = " + hostlist) logger.logit("Suffix = " + updatesuffix) # # update those hosts # h2 = httplib.HTTP(Dyndnshost) h2.putrequest("GET", updateprefix + hostlist + updatesuffix) h2.putheader("HOST: " + Dyndnshost) h2.putheader("USER-AGENT: " + Useragent) authstring = base64.encodestring(opt_username + ":" + opt_password) authstring = string.replace(authstring, "\012", "") h2.putheader("AUTHORIZATION", "Basic " + authstring) h2.endheaders() errcode, errmsg, headers = h2.getreply() # log the result logline = "http code = " + `errcode` logger.logit(logline) logline = "http msg = " + errmsg logger.logit(logline) # try to get the html text try: fp = h2.getfile() httpdata = fp.read() fp.close() except: httpdata = "No output from http request." # create the output file fp = open("zonecheck.html", "w") fp.write(httpdata) fp.close() logger.logit("zonecheck.html file created") # # check the result for fatal errors # res200 = string.find(httpdata, "200") res201 = string.find(httpdata, "201") if (res200 != -1) or (res201 != -1): logger.logit("Success") logger.logit(httpdata) # # write the update data to file # if localip != "": fp = open ('zonecheck.dat', 'w') fp.write(localip + "\n") # hostnames == updatehosts in the current version # but that may change in future versions of the client for host in hostnames: fp.write(host + "\n") fp.close() logger.logit("zonecheck.dat file created.") else: if os.path.isfile("zonecheck.dat"): os.unlink("zonecheck.dat") logger.logit("zonecheck.dat file removed.") else: logger.logexit("ERROR") logger.logexit(httpdata) # # save the error to an zonecheck.err file # fp = open ('zonecheck.err', 'w') fp.write(httpdata + "\n") fp.close() logger.logit("zonecheck.err file created.") sys.exit(-1)