Commits

Jeremy Sandell committed 7180c55

initial commit

  • Participants

Comments (0)

Files changed (11)

dyndns/__init__.py

+__doc__ = """DynDNS update client. Because I couldn't find a good one written
+in Python."""
+
+__version__ = (0,0,1)
+
+def get_version():
+    return '.'.join('%s' % v for v in __version__)

dyndns/conf/__init__.py

+"""
+Happily stolen from Django, though with a few tweaks, as this project
+doesn't need nearly as much functionality, and I'm only targetting new(ish)
+version of Python.
+
+"""
+
+from . import defaults
+
+__all__ = ('settings',)
+
+def isupper(s):
+    return s == s.upper()
+
+class Settings(object):
+    def __init__(self):
+        # get any of the uppercase defaults, and store it here.
+        self.__slurp(defaults)
+
+    def load(self, module):
+        self.__slurp(module)
+
+    def __slurp(self, module):
+        """
+        Get any uppercase value in the given module, and add it to our object.
+        """
+        valid = (setting for setting in dir(module) if isupper(setting))
+
+        for setting in valid:
+            setattr(self, setting, getattr(module, setting))
+
+    def __iter__(self):
+        """
+        Provides iteration facilities.
+
+        """
+
+        valid = (setting for setting in dir(self) if isupper(setting))
+
+        for setting in valid:
+            yield setting
+
+    __members__ = property(lambda self: self.__dir__())
+
+settings = Settings()

dyndns/conf/defaults.py

+DYNDNS_SERVER = 'members.dyndns.org'
+DYNDNS_CHECKIP = 'checkip.dyndns.org'
+
+# This can be any iterable; for performance reasons, I'd recommend a tuple.
+HOSTNAMES = (
+    'test.dyndns.org',
+)
+
+# Username for the dyndns service.
+USERNAME = 'test'
+
+# Password for the dyndns service.
+PASSWORD = 'test'
+
+# The address to connect to in order to receive your IP address.
+ROUTER_ADDRESS = '192.168.1.254'
+
+NAT = False
+DEBUG = False
+from logging import basicConfig, StreamHandler, Formatter, getLogger
+from dyndns.conf import settings
+from dyndns import get_version
+
+from urllib import urlencode
+from urllib2 import urlopen, Request, HTTPError
+from base64 import b64encode
+from os import uname
+from contextlib import closing
+
+from logging import getLogger
+
+default_logger = getLogger(__name__)
+
+__all__ = ('update',)
+
+def update(ip, logger=None):
+    """
+    Updates DynDNS.org with a new IP.
+    """
+
+    log = logger or default_logger
+
+    target = settings.DYNDNS_SERVER
+    username = settings.USERNAME
+    password = settings.PASSWORD
+    hostname = ','.join(settings.HOSTNAMES)
+
+    success_message = 'updated %(hostname)s to IP address %(ip)s' 
+    error_message = "Couldn't update %(hostname)s - %(error)s"
+    
+
+    credentials = b64encode(':'.join((username, password)))
+
+    headers = tuple({
+        'User-Agent' : '%(company)s - %(device)s - %(version)s' % dict(
+            company = 'pyDynDNS',
+            device = uname()[1],
+            version = get_version()
+        ),
+        'Authorization' : 'Basic %s' % credentials
+    }.items())
+
+
+    log.debug('updating host %(hostname)s with address %(ip)s' % locals())
+
+    params = dict(
+        hostname = hostname,
+        myip = ip,
+        wildcard = 'NOCHG',
+        mx = 'NOCHG',
+        backmx = 'NOCHG',
+    )
+
+    query = urlencode(params)
+    api_url = 'https://%(target)s:443/nic/update?%(query)s' % locals()
+
+    request = Request(api_url)
+
+    for header, value in headers:
+        request.add_header(header, value)
+
+    try:
+        with closing(urlopen(request)) as response:
+            status = response.code
+            info = response.info()
+
+            log.debug('status: %s' % status)
+            #log.debug('info: %s' % info)
+
+            if status == 200:
+                r = response.read()
+                log.debug('response: %s' % r)
+                result = r.split()
+
+                if len(result) > 0 and result[0] == 'good':
+                    log.debug(success_message % locals())
+                else:
+                    error = '%s - %s' % (result[0], info)
+                    log.error(error_message % locals())
+                    
+
+    except HTTPError, e:
+        log.error('HTTP error: %s' % e)

dyndns/utils/__init__.py

Empty file added.

dyndns/utils/external.py

+__doc__ = """
+Methods to get your IP address if you don't have a supported router in 
+utils.external
+
+"""
+
+from dyndns.conf import settings
+
+
+from urllib2 import Request, urlopen
+from urllib import urlencode
+from contextlib import closing
+import re
+
+
+__all__ = ('checkip',)
+
+checkip_address = 'http://%s/' % settings.DYNDNS_CHECKIP
+
+IP_PATTERN = r'Current IP Address: (?P<current_ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
+IP_RE = re.compile(IP_PATTERN)
+
+def checkip():
+    """Connects to dyndns's server and gets our current IP address.
+    If they return it in a format we understand, we return the IP address
+    as a string, otherwise, we return False.
+
+    """
+
+    request = Request(checkip_address)
+
+    with closing(urlopen(request)) as response:
+        html = response.read()
+        match = IP_RE.search(html)
+        if match:
+            return match.groupdict()['current_ip']
+
+    return False
+
+def test_matches():
+    TEST  = r'Current IP Address: 98.86.24.163'
+    TEST2 = r'Current IP Address: 192.168.2.10'
+    TEST3 = r'Current IP Address: 144.72.98.1'
+
+    matches = []
+
+    for pat in (TEST, TEST2, TEST3):
+        match = IP_RE.search(pat)
+
+        if match:
+            matches.append(match.groupdict()['current_ip'])
+
+    return tuple(matches)

dyndns/utils/internal.py

+from telnetlib import Telnet
+from time import sleep
+import re
+
+pattern = re.compile(r'^\s+?inet (?P<ip_address>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) netmask')
+
+# TODO: abstract this so we can hit more than just this one type of router.
+class Router(object):
+    """
+    Motorola Netopia Model 2210-02 DSL AnnexA Ethernet
+    Running Netopia SOC OS version 7.7.3 (build r11)
+    Multimode ADSL Capable
+
+    >>> router = Router('192.168.1.254')
+    >>> print router.addresses[-1]
+    127.0.0.1
+
+    """
+
+    def __init__(self, target, wait=None):
+        """
+
+        target
+            The hostname or IP address to connect to.
+        """
+        self.host = target
+        self.conn = Telnet(self.host)
+        wait = wait or .2
+
+        # wait a short while so we can make sure we get a response...
+        sleep(wait)
+        received = self.conn.read_very_eager()
+
+        # get the prompt, which we use elsewhere to determine end of message.
+        self.prompt = received.split('\r')[-1]
+
+    def send(self, command):
+        cmd = '%s\n' % command
+        conn = self.conn
+        conn.write(cmd)
+
+        read = conn.read_until
+        prompt = self.prompt
+
+        return (line.replace('\n', '') for line in read(prompt).split('\r')
+                if line.replace('\n',''))
+
+
+    @property
+    def addresses(self):
+        interfaces = self.send('show ip interfaces')
+        total = []
+
+        search = pattern.search
+
+        for line in interfaces:
+            match = search(line)
+            if match:
+                total.append(match.groupdict()['ip_address'])
+
+        return tuple(total)
+                    
+    @property
+    def external_address(self):
+        return self.addresses[-1]
+

example/config.py

+from logging import basicConfig, DEBUG, INFO, WARNING, ERROR, getLogger
+from os.path import abspath, dirname, join
+
+LOGGING_ROOT = abspath(dirname(__file__))
+
+# Defaults to false.
+DEBUG = True
+
+
+# Will log everything in /path/to/this/directory/dyndns.log
+basicConfig(
+    filename = join(LOGGING_ROOT, 'dyndns.log'),
+    format = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
+    datefmt='%a, %d %b %Y %H:%M:%S',
+    filemode = 'a'
+)
+
+# set logging to DEBUG.
+getLogger('').setLevel(DEBUG)
+
+# This defaults to 'test.dyndns.org'. Place all hostnames that you'd like
+# to be updated in this tuple.
+HOSTNAMES = (
+    'yourdomain.dyndns.org',
+)
+
+# If this isn't set, it defaults to 'test'
+USERNAME = 'your_username'
+
+# as does this.
+PASSWORD = 'your_password'

example/dyndns.log

+Sat, 14 May 2011 19:12:53 dyndns.update DEBUG    updating host setjmp.ath.cx with address 98.65.206.59
+Sat, 14 May 2011 19:12:53 dyndns.update DEBUG    status: 200
+Sat, 14 May 2011 19:12:53 dyndns.update DEBUG    response: nohost
+Sat, 14 May 2011 19:12:53 dyndns.update ERROR    Couldn't update setjmp.ath.cx - nohost - Date: Sat, 14 May 2011 23:12:53 GMT
+Server: Apache
+Content-Type: text/plain
+Connection: close
+Transfer-Encoding: chunked
+
+Sat, 14 May 2011 19:13:48 dyndns.update DEBUG    updating host setjmp.ath.cx with address 98.65.206.59
+Sat, 14 May 2011 19:13:48 dyndns.update DEBUG    status: 200
+Sat, 14 May 2011 19:13:48 dyndns.update DEBUG    response: good 98.65.206.59
+Sat, 14 May 2011 19:13:48 dyndns.update DEBUG    updated setjmp.ath.cx to IP address 98.65.206.59
+Sat, 14 May 2011 19:21:08 dyndns.update DEBUG    updating host setjmp.ath.cx with address 98.65.206.59
+Sat, 14 May 2011 19:21:08 dyndns.update DEBUG    status: 200
+Sat, 14 May 2011 19:21:08 dyndns.update DEBUG    response: good 98.65.206.59
+Sat, 14 May 2011 19:21:08 dyndns.update DEBUG    updated setjmp.ath.cx to IP address 98.65.206.59

example/updater.py

+from dyndns.conf import settings
+from dyndns.update import update
+from dyndns.utils.internal import Router
+from dyndns.utils.external import checkip
+import config
+
+def main():
+    settings.load(config)
+    nat = settings.NAT
+
+    router = None
+
+    if nat:
+        router = Router('192.168.1.254')
+        ip = router.external_address
+    else:
+        ip = checkip()
+
+    del router
+
+    update(ip)
+
+    
+if __name__ == '__main__':
+    main()