More HTTP status code fixes.
[oddments/ddns.git] / src / ddns / providers.py
1 #!/usr/bin/python
2 ###############################################################################
3 #                                                                             #
4 # ddns - A dynamic DNS client for IPFire                                      #
5 # Copyright (C) 2012 IPFire development team                                  #
6 #                                                                             #
7 # This program is free software: you can redistribute it and/or modify        #
8 # it under the terms of the GNU General Public License as published by        #
9 # the Free Software Foundation, either version 3 of the License, or           #
10 # (at your option) any later version.                                         #
11 #                                                                             #
12 # This program is distributed in the hope that it will be useful,             #
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of              #
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
15 # GNU General Public License for more details.                                #
16 #                                                                             #
17 # You should have received a copy of the GNU General Public License           #
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
19 #                                                                             #
20 ###############################################################################
21
22 import logging
23
24 from i18n import _
25
26 # Import all possible exception types.
27 from .errors import *
28
29 logger = logging.getLogger("ddns.providers")
30 logger.propagate = 1
31
32 class DDNSProvider(object):
33         INFO = {
34                 # A short string that uniquely identifies
35                 # this provider.
36                 "handle"    : None,
37
38                 # The full name of the provider.
39                 "name"      : None,
40
41                 # A weburl to the homepage of the provider.
42                 # (Where to register a new account?)
43                 "website"   : None,
44
45                 # A list of supported protocols.
46                 "protocols" : ["ipv6", "ipv4"],
47         }
48
49         DEFAULT_SETTINGS = {}
50
51         def __init__(self, core, **settings):
52                 self.core = core
53
54                 # Copy a set of default settings and
55                 # update them by those from the configuration file.
56                 self.settings = self.DEFAULT_SETTINGS.copy()
57                 self.settings.update(settings)
58
59         def __repr__(self):
60                 return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
61
62         def __cmp__(self, other):
63                 return cmp(self.hostname, other.hostname)
64
65         @property
66         def name(self):
67                 """
68                         Returns the name of the provider.
69                 """
70                 return self.INFO.get("name")
71
72         @property
73         def website(self):
74                 """
75                         Returns the website URL of the provider
76                         or None if that is not available.
77                 """
78                 return self.INFO.get("website", None)
79
80         @property
81         def handle(self):
82                 """
83                         Returns the handle of this provider.
84                 """
85                 return self.INFO.get("handle")
86
87         def get(self, key, default=None):
88                 """
89                         Get a setting from the settings dictionary.
90                 """
91                 return self.settings.get(key, default)
92
93         @property
94         def hostname(self):
95                 """
96                         Fast access to the hostname.
97                 """
98                 return self.get("hostname")
99
100         @property
101         def username(self):
102                 """
103                         Fast access to the username.
104                 """
105                 return self.get("username")
106
107         @property
108         def password(self):
109                 """
110                         Fast access to the password.
111                 """
112                 return self.get("password")
113
114         @property
115         def protocols(self):
116                 return self.INFO.get("protocols")
117
118         def __call__(self, force=False):
119                 if force:
120                         logger.info(_("Updating %s forced") % self.hostname)
121
122                 # Check if we actually need to update this host.
123                 elif self.is_uptodate(self.protocols):
124                         logger.info(_("%s is already up to date") % self.hostname)
125                         return
126
127                 # Execute the update.
128                 self.update()
129
130         def update(self):
131                 raise NotImplementedError
132
133         def is_uptodate(self, protos):
134                 """
135                         Returns True if this host is already up to date
136                         and does not need to change the IP address on the
137                         name server.
138                 """
139                 for proto in protos:
140                         addresses = self.core.system.resolve(self.hostname, proto)
141
142                         current_address = self.get_address(proto)
143
144                         if not current_address in addresses:
145                                 return False
146
147                 return True
148
149         def send_request(self, *args, **kwargs):
150                 """
151                         Proxy connection to the send request
152                         method.
153                 """
154                 return self.core.system.send_request(*args, **kwargs)
155
156         def get_address(self, proto):
157                 """
158                         Proxy method to get the current IP address.
159                 """
160                 return self.core.system.get_address(proto)
161
162
163 class DDNSProviderDHS(DDNSProvider):
164         INFO = {
165                 "handle"    : "dhs.org",
166                 "name"      : "DHS International",
167                 "website"   : "http://dhs.org/",
168                 "protocols" : ["ipv4",]
169         }
170
171         # No information about the used update api provided on webpage,
172         # grabed from source code of ez-ipudate.
173         url = "http://members.dhs.org/nic/hosts"
174
175         def update(self):
176                 url = self.url % {
177                         "username" : self.username,
178                         "password" : self.password,
179                 }
180
181                 data = {
182                         "domain"       : self.hostname,
183                         "ip"           : self.get_address("ipv4"),
184                         "hostcmd"      : "edit",
185                         "hostcmdstage" : "2",
186                         "type"         : "4",
187                 }
188
189                 # Send update to the server.
190                 response = self.send_request(url, username=self.username, password=self.password,
191                         data=data)
192
193                 # Handle success messages.
194                 if response.code == 200:
195                         return
196
197                 # Handle error codes.
198                 elif response.code == 401:
199                         raise DDNSAuthenticationError
200
201                 # If we got here, some other update error happened.
202                 raise DDNSUpdateError
203
204
205 class DDNSProviderDNSpark(DDNSProvider):
206         INFO = {
207                 "handle"    : "dnspark.com",
208                 "name"      : "DNS Park",
209                 "website"   : "http://dnspark.com/",
210                 "protocols" : ["ipv4",]
211         }
212
213         # Informations to the used api can be found here:
214         # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
215         url = "https://control.dnspark.com/api/dynamic/update.php"
216
217         def update(self):
218                 url = self.url % {
219                         "username" : self.username,
220                         "password" : self.password,
221                 }
222
223                 data = {
224                         "domain" : self.hostname,
225                         "ip"     : self.get_address("ipv4"),
226                 }
227
228                 # Send update to the server.
229                 response = self.send_request(url, username=self.username, password=self.password,
230                         data=data)
231
232                 # Get the full response message.
233                 output = response.read()
234
235                 # Handle success messages.
236                 if output.startswith("ok") or output.startswith("nochange"):
237                         return
238
239                 # Handle error codes.
240                 if output == "unauth":
241                         raise DDNSAuthenticationError
242                 elif output == "abuse":
243                         raise DDNSAbuseError
244                 elif output == "blocked":
245                         raise DDNSBlockedError
246                 elif output == "nofqdn":
247                         raise DDNSRequestError(_("No valid FQDN was given."))
248                 elif output == "nohost":
249                         raise DDNSRequestError(_("Invalid hostname specified."))
250                 elif output == "notdyn":
251                         raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
252                 elif output == "invalid":
253                         raise DDNSRequestError(_("Invalid IP address has been sent."))
254
255                 # If we got here, some other update error happened.
256                 raise DDNSUpdateError
257
258                 
259 class DDNSProviderLightningWireLabs(DDNSProvider):
260         INFO = {
261                 "handle"    : "dns.lightningwirelabs.com",
262                 "name"      : "Lightning Wire Labs",
263                 "website"   : "http://dns.lightningwirelabs.com/",
264                 "protocols" : ["ipv6", "ipv4",]
265         }
266
267         # Information about the format of the HTTPS request is to be found
268         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
269         url = "https://dns.lightningwirelabs.com/update"
270
271         @property
272         def token(self):
273                 """
274                         Fast access to the token.
275                 """
276                 return self.get("token")
277
278         def update(self):
279                 data =  {
280                         "hostname" : self.hostname,
281                 }
282
283                 # Check if we update an IPv6 address.
284                 address6 = self.get_address("ipv6")
285                 if address6:
286                         data["address6"] = address6
287
288                 # Check if we update an IPv4 address.
289                 address4 = self.get_address("ipv4")
290                 if address4:
291                         data["address4"] = address4
292
293                 # Raise an error if none address is given.
294                 if not data.has_key("address6") and not data.has_key("address4"):
295                         raise DDNSConfigurationError
296
297                 # Check if a token has been set.
298                 if self.token:
299                         data["token"] = self.token
300
301                 # Check for username and password.
302                 elif self.username and self.password:
303                         data.update({
304                                 "username" : self.username,
305                                 "password" : self.password,
306                         })
307
308                 # Raise an error if no auth details are given.
309                 else:
310                         raise DDNSConfigurationError
311
312                 # Send update to the server.
313                 response = self.send_request(self.url, data=data)
314
315                 # Handle success messages.
316                 if response.code == 200:
317                         return
318
319                 # Handle error codes.
320                 if response.code == 403:
321                         raise DDNSAuthenticationError
322                 elif response.code == 400:
323                         raise DDNSRequestError
324
325                 # If we got here, some other update error happened.
326                 raise DDNSUpdateError
327
328
329 class DDNSProviderNOIP(DDNSProvider):
330         INFO = {
331                 "handle"    : "no-ip.com",
332                 "name"      : "No-IP",
333                 "website"   : "http://www.no-ip.com/",
334                 "protocols" : ["ipv4",]
335         }
336
337         # Information about the format of the HTTP request is to be found
338         # here: http://www.no-ip.com/integrate/request and
339         # here: http://www.no-ip.com/integrate/response
340
341         url = "http://%(username)s:%(password)s@dynupdate.no-ip.com/nic/update"
342
343         def update(self):
344                 url = self.url % {
345                         "username" : self.username,
346                         "password" : self.password,
347                 }
348
349                 data = {
350                         "hostname" : self.hostname,
351                         "address"  : self.get_address("ipv4"),
352                 }
353
354                 # Send update to the server.
355                 response = self.send_request(url, data=data)
356
357                 # Get the full response message.
358                 output = response.read()
359
360                 # Handle success messages.
361                 if output.startswith("good") or output.startswith("nochg"):
362                         return
363
364                 # Handle error codes.
365                 if output == "badauth":
366                         raise DDNSAuthenticationError
367                 elif output == "aduse":
368                         raise DDNSAbuseError
369                 elif output == "911":
370                         raise DDNSInternalServerError
371
372                 # If we got here, some other update error happened.
373                 raise DDNSUpdateError
374
375
376 class DDNSProviderSelfhost(DDNSProvider):
377         INFO = {
378                 "handle"    : "selfhost.de",
379                 "name"      : "Selfhost.de",
380                 "website"   : "http://www.selfhost.de/",
381                 "protocols" : ["ipv4",],
382         }
383
384         url = "https://carol.selfhost.de/update"
385
386         def update(self):
387                 data = {
388                         "username" : self.username,
389                         "password" : self.password,
390                         "textmodi" : "1",
391                 }
392
393                 response = self.send_request(self.url, data=data)
394
395                 match = re.search("status=20(0|4)", response.read())
396                 if not match:
397                         raise DDNSUpdateError