2 """Dynamic DNS script. Expects URLs from routers in the form
3 http://dyndns.colgarra.priv.at/nic/update?username=<username>&password=<pass>&hostname=<domain>&myip=<ipaddr>
10 from subprocess import call
15 PASSWORD = 'hygCithOrs5'
16 ZONE = '.dyn.colgarra.priv.at'
26 # Base class for our exceptions
27 class DynDnsError(Exception):
30 class AuthError(DynDnsError):
31 returncode = 'badauth'
33 class CredentialsMissing(AuthError):
36 class UsernameMissing(AuthError):
39 class UsernameInvalid(AuthError):
42 class PasswordMissing(AuthError):
45 class PasswordWrong(AuthError):
48 class AuthWrongMethod(AuthError):
49 returncode = 'wrongauthmethod' # not documented at dyn.com
51 class AuthBasicError(AuthError):
52 returncode = 'authbasicerror' # not documented at dyn.com
54 class HostnameError(DynDnsError):
55 returncode = 'notfqdn'
57 class HostnameMissing(HostnameError):
60 class PasswordWrong(HostnameError):
63 class MyipError(DynDnsError):
64 returncode = 'badip' # not documented at dyn.com
66 class MyipMissing(MyipError):
69 class MyipInvalid(MyipError):
72 class OfflineInvalid(DynDnsError):
73 returncode = 'badparam' # not documented at dyn.com
76 fields = cgi.FieldStorage()
78 # the following fields are supported by most dyndns providers
79 # if a parameter is not provided, the .getvalue method returns None
80 username = fields.getvalue('username')
81 password = fields.getvalue('password')
82 hostname = fields.getvalue('hostname')
83 myip = fields.getvalue('myip')
84 wildcard = fields.getvalue('wildcard')
85 mx = fields.getvalue('mx')
86 backmx = fields.getvalue('backmx')
87 offline = fields.getvalue('offline')
88 system = fields.getvalue('system')
89 url = fields.getvalue('url')
94 auth = os.environ.get('HTTP_AUTHORIZATION') # auth == 'Basic cGhpbGlwcDpka2ZhamRrZg=='
95 if auth: # empty string if HTTP_AUTHORIZATION not present
96 auth_parts = auth.split(' ')
98 if len(auth_parts) != 2 or auth_parts[0] != auth_method:
99 raise AuthWrongMethod()
101 auth_decoded = base64.b64decode(auth_parts[1]) # auth_decoded == 'philipp:dkfajdkf'
103 raise AuthBasicError()
104 auth_decoded_parts = auth_decoded.split(':')
105 if len(auth_decoded_parts) != 2:
106 raise AuthBasicError()
107 username, password = auth_decoded_parts
109 # check username and password
110 if username is None and password is None:
111 raise CredentialsMissing()
115 raise UsernameMissing()
117 user_info = pwd.getpwnam(username)
119 raise UsernameInvalid()
120 if user_info.pw_uid < 1000:
121 raise UsernameInvalid()
125 raise PasswordMissing()
126 if password != PASSWORD:
127 raise PasswordWrong()
131 raise HostnameMissing()
132 if re.match(r'[-0-9a-z]+(\.[-0-9a-z]+)*$', hostname) is None:
133 raise HostnameInvalid()
136 hostname = hostname.strip()
137 if hostname.endswith(ZONE):
138 hostname = hostname[:-len(ZONE)]
141 if offline is None or offline.lower() == 'no':
143 elif offline.lower() == 'yes':
146 raise OfflineInvalid()
151 # try HTTP_X_FORWARDED_FOR
152 myip = os.environ.get('HTTP_X_FORWARDED_FOR')
153 if not myip: # empty string if not present
155 myip = os.environ.get('REMOTE_ADDR')
156 if not myip: # empty string if not present
159 ip = ipaddr.IPAddress(myip) # throws an exception if the IP address is not valid
162 if isinstance(ip, ipaddr.IPv4Address):
164 elif isinstance(ip, ipaddr.IPv6Address):
167 raise MyipInvalid() # should never happen
171 call(['sudo', '/usr/local/bin/nsupdate_dyndns', '--delete', hostname])
173 call(['sudo', '/usr/local/bin/nsupdate_dyndns', '--ip', myip, hostname])
176 print "Content-Type: text/html"
179 # Note: we should return 'nochg' in case the IP has not changed, however we don't know yet.
182 except CredentialsMissing as error:
183 print "Content-Type: text/html"
184 print "Status: 401 Unauthorized"
185 print "WWW-Authenticate: Basic realm='tdyndns'"
188 except DynDnsError as error:
189 print "Content-Type: text/html"
190 print "Status: 200 OK"
192 print error.returncode