Now in both scripts only fqdn are allowed.
[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 DEBUG = False
17
18
19 # Just for debugging:
20 if DEBUG:
21         import cgitb
22         cgitb.enable()
23
24
25 # Base class for our exceptions
26 class DynDnsError(Exception):
27         pass
28
29 class AuthError(DynDnsError):
30         returncode = 'badauth'
31
32 class CredentialsMissing(AuthError):
33         pass
34
35 class UsernameMissing(AuthError):
36         pass
37
38 class UsernameInvalid(AuthError):
39         pass
40
41 class PasswordMissing(AuthError):
42         pass
43
44 class PasswordWrong(AuthError):
45         pass
46
47 class AuthWrongMethod(AuthError):
48         returncode = 'wrongauthmethod' # not documented at dyn.com
49
50 class AuthBasicError(AuthError):
51         returncode = 'authbasicerror' # not documented at dyn.com
52
53 class HostnameError(DynDnsError):
54         returncode = 'notfqdn'
55
56 class HostnameMissing(HostnameError):
57         pass
58
59 class PasswordWrong(HostnameError):
60         pass
61
62 class MyipError(DynDnsError):
63         returncode = 'badip' # not documented at dyn.com
64
65 class MyipMissing(MyipError):
66         pass
67
68 class MyipInvalid(MyipError):
69         pass
70
71 class OfflineInvalid(DynDnsError):
72         returncode = 'badparam' # not documented at dyn.com
73
74
75 fields = cgi.FieldStorage()
76
77 # the following fields are supported by most dyndns providers
78 # if a parameter is not provided, the .getvalue method returns None
79 username = fields.getvalue('username')
80 password = fields.getvalue('password')
81 hostname = fields.getvalue('hostname')
82 myip     = fields.getvalue('myip')
83 wildcard = fields.getvalue('wildcard')
84 mx       = fields.getvalue('mx')
85 backmx   = fields.getvalue('backmx')
86 offline  = fields.getvalue('offline')
87 system   = fields.getvalue('system')
88 url      = fields.getvalue('url')
89
90
91 try:
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         # check username and password
109         if username is None and password is None:
110                 raise CredentialsMissing()
111
112         # check username
113         if username is None:
114                 raise UsernameMissing()
115         try:
116                 user_info = pwd.getpwnam(username)
117         except KeyError:
118                 raise UsernameInvalid()
119         if user_info.pw_uid < 1000:
120                 raise UsernameInvalid()
121
122         # check password
123         if password is None:
124                 raise PasswordMissing()
125         if password != PASSWORD:
126                 raise PasswordWrong()
127
128         # check hostname
129         if hostname is None:
130                 raise HostnameMissing()
131         if re.match(r'[-0-9a-z]+(\.[-0-9a-z]+)*$', hostname) is None:
132                 raise HostnameInvalid()
133
134         # strip zone
135         hostname = hostname.strip()
136
137         # check offline
138         if offline is None or offline.lower() == 'no':
139                 offline = False
140         elif offline.lower() == 'yes':
141                 offline = True
142         else:
143                 raise OfflineInvalid()
144
145         # check IP address
146         if not offline:
147                 if myip is None:
148                         # try HTTP_X_FORWARDED_FOR
149                         myip = os.environ.get('HTTP_X_FORWARDED_FOR')
150                         if not myip: # empty string if not present
151                                 # try REMOTE_ADDR
152                                 myip = os.environ.get('REMOTE_ADDR')
153                                 if not myip: # empty string if not present
154                                         raise MyipMissing()
155                 try:
156                         ip = ipaddr.IPAddress(myip) # throws an exception if the IP address is not valid
157                 except ValueError:
158                         raise MyipInvalid()
159                 if isinstance(ip, ipaddr.IPv4Address):
160                         iptype = 'A'
161                 elif isinstance(ip, ipaddr.IPv6Address):
162                         iptype = 'AAAA'
163                 else:
164                         raise MyipInvalid() # should never happen
165
166         # update bind
167         if offline:
168                 call(['sudo', '/usr/local/bin/nsupdate_dyndns', '--delete', hostname])
169         else:
170                 call(['sudo', '/usr/local/bin/nsupdate_dyndns', '--ip', myip, hostname])
171
172         # return success
173         print "Content-Type: text/html"
174         print
175         print "good"
176         # Note: we should return 'nochg' in case the IP has not changed, however we don't know yet.
177
178
179 except CredentialsMissing as error:
180         print "Content-Type: text/html"
181         print "Status: 401 Unauthorized"
182         print "WWW-Authenticate: Basic realm='tdyndns'"
183         print
184
185 except DynDnsError as error:
186         print "Content-Type: text/html"
187         print "Status: 200 OK"
188         print
189         print error.returncode
190