This script is now in Python.
[toast/tdyndns.git] / cgi-bin / dyndns.py
1 #!/usr/bin/python2.7
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>
4 """
5 import os
6 import re
7 import cgi
8 import pwd
9 import base64
10 from subprocess import call
11 import ipaddr
12
13
14 # Configuration
15 PASSWORD = 'hygCithOrs5'
16 ZONE = '.dyn.colgarra.priv.at'
17 DEBUG = False
18
19
20 # Just for debugging:
21 if DEBUG:
22         import cgitb
23         cgitb.enable()
24
25
26 # Base class for our exceptions
27 class DynDnsError(Exception):
28         pass
29
30 class AuthError(DynDnsError):
31         returncode = 'badauth'
32
33 class CredentialsMissing(AuthError):
34         pass
35
36 class UsernameMissing(AuthError):
37         pass
38
39 class UsernameInvalid(AuthError):
40         pass
41
42 class PasswordMissing(AuthError):
43         pass
44
45 class PasswordWrong(AuthError):
46         pass
47
48 class AuthWrongMethod(AuthError):
49         returncode = 'wrongauthmethod' # not documented at dyn.com
50
51 class AuthBasicError(AuthError):
52         returncode = 'authbasicerror' # not documented at dyn.com
53
54 class HostnameError(DynDnsError):
55         returncode = 'notfqdn'
56
57 class HostnameMissing(HostnameError):
58         pass
59
60 class PasswordWrong(HostnameError):
61         pass
62
63 class MyipError(DynDnsError):
64         returncode = 'badip' # not documented at dyn.com
65
66 class MyipMissing(MyipError):
67         pass
68
69 class MyipInvalid(MyipError):
70         pass
71
72 class OfflineInvalid(DynDnsError):
73         returncode = 'badparam' # not documented at dyn.com
74
75
76 fields = cgi.FieldStorage()
77
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')
90
91
92 # Optional Auth Basic
93 auth = os.environ.get('HTTP_AUTHORIZATION') # auth == 'Basic cGhpbGlwcDpka2ZhamRrZg=='
94 if auth: # empty string if HTTP_AUTHORIZATION not present
95         auth_parts = auth.split(' ')
96         auth_method = 'Basic'
97         if len(auth_parts) != 2 or auth_parts[0] != auth_method:
98                 raise AuthWrongMethod()
99         try:
100                 auth_decoded = base64.b64decode(auth_parts[1]) # auth_decoded == 'philipp:dkfajdkf'
101         except TypeError:
102                 raise AuthBasicError()
103         auth_decoded_parts = auth_decoded.split(':')
104         if len(auth_decoded_parts) != 2:
105                 raise AuthBasicError()
106         username, password = auth_decoded_parts
107
108
109 try:
110         # check username and password
111         if username is None and password is None:
112                 raise CredentialsMissing()
113
114         # check username
115         if username is None:
116                 raise UsernameMissing()
117         try:
118                 user_info = pwd.getpwnam(username)
119         except KeyError:
120                 raise UsernameInvalid()
121         if user_info.pw_uid < 1000:
122                 raise UsernameInvalid()
123
124         # check password
125         if password is None:
126                 raise PasswordMissing()
127         if password != PASSWORD:
128                 raise PasswordWrong()
129
130         # check hostname
131         if hostname is None:
132                 raise HostnameMissing()
133         if re.match(r'[-0-9a-z]+(\.[-0-9a-z]+)*$', hostname) is None:
134                 raise HostnameInvalid()
135
136         # strip zone
137         hostname = hostname.strip()
138         if hostname.endswith(ZONE):
139                 hostname = hostname[:-len(ZONE)]
140
141         # check offline
142         if offline is None or offline.lower() == 'no':
143                 offline = False
144         elif offline.lower() == 'yes':
145                 offline = True
146         else:
147                 raise OfflineInvalid()
148
149         # check IP address
150         if not offline:
151                 if myip is None:
152                         raise MyipMissing()
153                 try:
154                         ip = ipaddr.IPAddress(myip) # throws an exception if the IP address is not valid
155                 except ValueError:
156                         raise MyipInvalid()
157                 if isinstance(ip, ipaddr.IPv4Address):
158                         iptype = 'A'
159                 elif isinstance(ip, ipaddr.IPv6Address):
160                         iptype = 'AAAA'
161                 else:
162                         raise MyipInvalid() # should never happen
163
164         # update bind
165         if offline:
166                 call(['sudo', '/usr/local/bin/nsupdate_dyndns_del', hostname, 'A'])
167                 call(['sudo', '/usr/local/bin/nsupdate_dyndns_del', hostname, 'AAAA'])
168         else:
169                 call(['sudo', '/usr/local/bin/nsupdate_dyndns', hostname, myip, iptype])
170
171         # return success
172         print "Content-Type: text/html"
173         print
174         print "good"
175         # Note: we should return 'nochg' in case the IP has not changed, however we don't know yet.
176
177
178 except CredentialsMissing as error:
179         print "Content-Type: text/html"
180         print "Status: 401 Unauthorized"
181         print "WWW-Authenticate: Basic realm='tdyndns'"
182         print
183
184 except DynDnsError as error:
185         print "Content-Type: text/html"
186         print "Status: 200 OK"
187         print
188         print error.returncode
189