#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, Markus Stumpf # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of Markus Stumpf nor the names of its contributors may # be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. import os import sys import ConfigParser import getopt import httplib from xml.dom import minidom import time try: import dns.query import dns.tsigkeyring import dns.update except: print "Error: need dnspython package to run. see http://www.dnspython.org/" sys.exit(1) __version__ = '1.0' __author__ = 'Markus Stumpf ' TFORM_RFC3339 = '%FT%T' TFORM_RFC3339 = '%F %T' DEFAULT_CONFIGFILE = "/etc/pydnsup/pydnsup.conf" class dnsupdater: def __init__(self, configfile=DEFAULT_CONFIGFILE, debug=False): self._o_debug = debug self.readConfig(configfile) def __writemsg(self, message): sys.stderr.write(message.encode('utf-8')) if "\n" != message[-1]: sys.stderr.write("\n") def _error(self, message): self.__writemsg("Error: " + message) def _info(self, message): self.__writemsg("Info: " + message) def _debug(self, message): self.__writemsg("Debug: " + message) def readConfig(self, configfile): '''read the configuration file''' config = ConfigParser.ConfigParser() if not os.path.isfile(configfile): self._error("configfile '%s' does not exist" % (configfile)) sys.exit(1) if self._o_debug: self._debug("parsing config file '%s' ..." %(configfile)) try: config.read(configfile) self._dns_keyname = config.get("dns", "keyname") self._dns_keyvalue = config.get("dns", "keyvalue") self._dns_host = config.get("dns", "host") self._dns_domain = config.get("dns", "domain") self._dns_ttl = config.getint("dns", "ttl") self._dns_serverip = config.get("dns", "serverip") self._dns_addtxt = config.getboolean("dns", "addtxt") self._dns_timeout = config.getfloat("dns", "timeout") self._router_ip = config.get("router", "ip") self._router_port = config.getint("router", "port") self._router_http_req = config.get("router", "http_req") except Exception, e: self._error("error reading configuration: %s" % (e)) sys.exit(1) def upnp_info(self, action, result): '''get the external IP address via UPnP from the router''' reqBody = """\ """ % (action) if self._o_debug: self._debug("""Request to UPnP-Server: ------------------------------------------------------------------------ %s ------------------------------------------------------------------------ """ % (reqBody)) # 'SOAPACTION' : "urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress" mh = { 'User-Agent' : 'pyDNSUpdate/%s' % (__version__), 'Content-type' : 'text/xml; charset="utf-8"', 'Content-length' : len(reqBody), 'Connection' : 'close', 'SOAPACTION' : "urn:schemas-upnp-org:service:WANIPConnection:1#%s" % (action) } if self._o_debug: self._debug("HTTP Request:") self._debug("\trouter_ip:\t\t%s\n" % (self._router_ip)) self._debug("\trouter_port:\t\t%s\n" % (self._router_port)) self._debug("\trequest:\t\t%s\n" % (self._router_http_req)) try: conn = httplib.HTTPConnection(self._router_ip, port=self._router_port) conn.request("POST", self._router_http_req, body=reqBody, headers=mh) except: raise sys.exit(1) res = conn.getresponse() conn.close() resBody = '' while 1: r = res.read() if '' == r: break resBody += r if self._o_debug: self._debug("""Answer from UPnP-Server: ------------------------------------------------------------------------ %s ------------------------------------------------------------------------ """ % ( resBody)) if 200 != res.status: self._error("HTTP request failed: code=%d" % (res.status)) sys.exit(1) try: xmldoc = minidom.parseString(resBody) except: raise sys.exit(1) nodelist = xmldoc.getElementsByTagName(result) if 0 == len(nodelist): self._error("parsing XML answer: node '%s' not found" % (result)) sys.exit(1) if self._o_debug: self._debug("found IP address: %s" % (nodelist[0].firstChild.data)) return(nodelist[0].firstChild.data) # ---------------------------------------------------------------- def dns_delete(self): '''send a delete request to the DNS server''' keyring = dns.tsigkeyring.from_text({ self._dns_keyname : self._dns_keyvalue }) if self._o_debug: self._debug("DNS Update:") self._debug("\tdomain:\t\t%s\n" % (self._dns_domain)) self._debug("\thost:\t\t%s\n" % (self._dns_host)) self._debug("\tserverip:\t%s\n" % (self._dns_serverip)) self._debug("\tkeyname:\t%s\n" % (self._dns_keyname)) self._debug("\ttimeout:\t%s\n" % (self._dns_timeout)) update = dns.update.Update(self._dns_domain, keyring=keyring) update.delete(self._dns_host) response = dns.query.tcp(update, self._dns_serverip) r = dns.rcode.to_text(response.rcode()) if self._o_debug: self._debug("DNS update rcode: %s\n" % (r)) if "NOERROR" != r: self._error("error updating DNS record(s): %s" % (r)) sys.exit(1) # ---------------------------------------------------------------- def dns_update(self, ipaddress): '''send an update request to the DNS server''' keyring = dns.tsigkeyring.from_text({ self._dns_keyname : self._dns_keyvalue }) tstamp = time.strftime(TFORM_RFC3339, time.localtime()) if self._o_debug: self._debug("DNS Update:") self._debug("\tdomain:\t\t%s\n" % (self._dns_domain)) self._debug("\thost:\t\t%s\n" % (self._dns_host)) self._debug("\tserverip:\t%s\n" % (self._dns_serverip)) self._debug("\tttl:\t\t%s\n" % (self._dns_ttl)) self._debug("\tkeyname:\t%s\n" % (self._dns_keyname)) self._debug("\ttimestamp:\t%s\n" % (tstamp)) self._debug("\tipaddress:\t%s\n" % (ipaddress)) self._debug("\ttimeout:\t%s\n" % (self._dns_timeout)) self._debug("\taddtxt:\t\t%s\n" % (self._dns_addtxt)) update = dns.update.Update(self._dns_domain, keyring=keyring) update.replace(self._dns_host, self._dns_ttl, 'A', str(ipaddress)) if self._dns_addtxt: update.replace(self._dns_host, self._dns_ttl, 'TXT', str(tstamp)) response = dns.query.tcp(update, self._dns_serverip) r = dns.rcode.to_text(response.rcode()) if self._o_debug: self._debug("DNS update rcode: %s\n" % (r)) if "NOERROR" != r: self._error("error updating DNS record(s): %s" % (r)) sys.exit(1) # ------------------------------------------------------------------------ if __name__ == "__main__": configfile = DEFAULT_CONFIGFILE o_delete = False o_verbose = False o_debug = False o_upnp_only = False o_help = False ipa = None try: gopts, cmds = getopt.getopt(sys.argv[1:], 'c:dhi:nv', ['help', 'delete']) except getopt.GetoptError, e: print "Error:", e sys.exit(1) if cmds: print "Error: extraneous parameter(s): %s" % (", ".join(cmds)) sys.exit(1) for o,a in gopts: if "-c" == o: configfile = a elif "-d" == o: o_debug = True elif "--delete" == o: o_delete = True elif "-h" == o or "--help" == o: o_help = True elif "-i" == o: ipa = a elif "-n" == o: o_upnp_only = True elif "-v" == o: o_verbose = True else: print "Huh? %s" % (o) print "Error: internal programming error" sys.exit(1) if o_help: print """ Usage: %s [ options ] options: -d debug; prints some info for trouble shooting -h help; this message --help help; this message -n no dns update; just request the IP from the router -v verbose; outputs the IP address --delete delete record(s) for the host -c cfile specifies an alternate configfile -i ipaddress specifies an IP address to use for dns update skips requesting the IP address from the router """ % (sys.argv[0]) sys.exit(0) dnsu = dnsupdater(configfile=configfile, debug=o_debug) if not ipa and not o_delete: # get ConnectionStatus to see if the router is connected status = dnsu.upnp_info("GetStatusInfo", "NewConnectionStatus") if "Connected" != status: print "Error: ConnectionStatus of router is '%s'" % (status) sys.exit(1) # get the external IP address ipa = dnsu.upnp_info("GetExternalIPAddress", "NewExternalIPAddress") if o_verbose and not o_delete: print "IP-Address: %s" % (ipa) if not o_upnp_only: if o_delete: dnsu.dns_delete() else: dnsu.dns_update(ipa) sys.exit(0)