#!/usr/bin/python2.7
"""Dynamic DNS script. Expects URLs from routers in the form
-https://info.colgarra.priv.at/dyndns/dyndns.py?username=<username>&password=<pass>&hostname=<domain>&myip=<ipaddr>
+http://dyndns.colgarra.priv.at/nic/update?username=<username>&password=<pass>&hostname=<domain>&myip=<ipaddr>
"""
-
+import os
import re
import cgi
import pwd
+import base64
from subprocess import call
import ipaddr
# Configuration
PASSWORD = 'hygCithOrs5'
-ZONE = '.dyn.colgarra.priv.at'
DEBUG = False
cgitb.enable()
+# Base class for our exceptions
+class DynDnsError(Exception):
+ pass
+
+class AuthError(DynDnsError):
+ returncode = 'badauth'
+
+class CredentialsMissing(AuthError):
+ pass
+
+class UsernameMissing(AuthError):
+ pass
+
+class UsernameInvalid(AuthError):
+ pass
+
+class PasswordMissing(AuthError):
+ pass
+
+class PasswordWrong(AuthError):
+ pass
+
+class AuthWrongMethod(AuthError):
+ returncode = 'wrongauthmethod' # not documented at dyn.com
+
+class AuthBasicError(AuthError):
+ returncode = 'authbasicerror' # not documented at dyn.com
+
+class HostnameError(DynDnsError):
+ returncode = 'notfqdn'
+
+class HostnameMissing(HostnameError):
+ pass
+
+class PasswordWrong(HostnameError):
+ pass
+
+class MyipError(DynDnsError):
+ returncode = 'badip' # not documented at dyn.com
+
+class MyipMissing(MyipError):
+ pass
+
+class MyipInvalid(MyipError):
+ pass
+
+class OfflineInvalid(DynDnsError):
+ returncode = 'badparam' # not documented at dyn.com
+
+class NsupdateError(DynDnsError):
+ returncode = 'nohost'
+
+
fields = cgi.FieldStorage()
# the following fields are supported by most dyndns providers
mx = fields.getvalue('mx')
backmx = fields.getvalue('backmx')
offline = fields.getvalue('offline')
+system = fields.getvalue('system')
+url = fields.getvalue('url')
try:
+ # Optional Auth Basic
+ auth = os.environ.get('HTTP_AUTHORIZATION') # auth == 'Basic cGhpbGlwcDpka2ZhamRrZg=='
+ if auth: # empty string if HTTP_AUTHORIZATION not present
+ auth_parts = auth.split(' ')
+ auth_method = 'Basic'
+ if len(auth_parts) != 2 or auth_parts[0] != auth_method:
+ raise AuthWrongMethod()
+ try:
+ auth_decoded = base64.b64decode(auth_parts[1]) # auth_decoded == 'philipp:dkfajdkf'
+ except TypeError:
+ raise AuthBasicError()
+ auth_decoded_parts = auth_decoded.split(':')
+ if len(auth_decoded_parts) != 2:
+ raise AuthBasicError()
+ username, password = auth_decoded_parts
+
+ # check username and password
+ if username is None and password is None:
+ raise CredentialsMissing()
+
# check username
- user_info = pwd.getpwnam(username) # returns a key error if the user does not exist
+ if username is None:
+ raise UsernameMissing()
+ try:
+ user_info = pwd.getpwnam(username)
+ except KeyError:
+ raise UsernameInvalid()
if user_info.pw_uid < 1000:
- raise RuntimeError('Invalid user name')
+ raise UsernameInvalid()
# check password
+ if password is None:
+ raise PasswordMissing()
if password != PASSWORD:
- raise RuntimeError('Invalid password')
+ raise PasswordWrong()
# check hostname
+ if hostname is None:
+ raise HostnameMissing()
if re.match(r'[-0-9a-z]+(\.[-0-9a-z]+)*$', hostname) is None:
- raise RuntimeError('Invalid host name')
+ raise HostnameInvalid()
# strip zone
hostname = hostname.strip()
- if hostname.endswith(ZONE):
- hostname = hostname[:-len(ZONE)]
- # check IP address
- ip = ipaddr.IPAddress(myip) # throws axception if the IP address is not valid
- if isinstance(ip, ipaddr.IPv4Address):
- type = 'A'
- elif isinstance(ip, ipaddr.IPv6Address):
- type = 'AAAA'
+ # check offline
+ if offline is None or offline.lower() == 'no':
+ offline = False
+ elif offline.lower() == 'yes':
+ offline = True
else:
- raise RuntimeError('Unknown IP address type')
+ raise OfflineInvalid()
- # access granted
+ # check IP address
+ if not offline:
+ if myip is None:
+ # try HTTP_X_FORWARDED_FOR
+ myip = os.environ.get('HTTP_X_FORWARDED_FOR')
+ if not myip: # empty string if not present
+ # try REMOTE_ADDR
+ myip = os.environ.get('REMOTE_ADDR')
+ if not myip: # empty string if not present
+ raise MyipMissing()
+ if not myip is None:
+ try:
+ ipaddr.IPAddress(myip) # throws an exception if the IP address is not valid
+ except ValueError:
+ raise MyipInvalid()
+
+ # update bind
+ call_params = ['sudo', 'nsupdate_dyndns']
+ if offline:
+ call_params.append('--delete')
+ if myip is not None:
+ call_params.extend(['--ip', myip])
+ call_params.append(hostname)
+ retcode = call(call_params)
+ if retcode != 0:
+ raise NsupdateError()
+
+ # return success
print "Content-Type: text/html"
print
- call(['sudo', '/usr/local/bin/nsupdate_dyndns', hostname, myip, type])
- print "OK"
+ print "good"
+ # Note: we should return 'nochg' in case the IP has not changed, however we don't know yet.
-except:
- # access denied
+except CredentialsMissing as error:
+ print "Content-Type: text/html"
+ print "Status: 401 Unauthorized"
+ print "WWW-Authenticate: Basic realm='tdyndns'"
+ print
+
+except DynDnsError as error:
print "Content-Type: text/html"
- print "Status: 403 Forbidden"
+ print "Status: 200 OK"
print
- print "Denied"
+ print error.returncode