9eea16de606641580f6484b7f61d94dbbe90d324
[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         @property
119         def token(self):
120                 """
121                         Fast access to the token.
122                 """
123                 return self.get("token")
124
125         def __call__(self, force=False):
126                 if force:
127                         logger.info(_("Updating %s forced") % self.hostname)
128
129                 # Check if we actually need to update this host.
130                 elif self.is_uptodate(self.protocols):
131                         logger.info(_("%s is already up to date") % self.hostname)
132                         return
133
134                 # Execute the update.
135                 self.update()
136
137         def update(self):
138                 raise NotImplementedError
139
140         def is_uptodate(self, protos):
141                 """
142                         Returns True if this host is already up to date
143                         and does not need to change the IP address on the
144                         name server.
145                 """
146                 for proto in protos:
147                         addresses = self.core.system.resolve(self.hostname, proto)
148
149                         current_address = self.get_address(proto)
150
151                         if not current_address in addresses:
152                                 return False
153
154                 return True
155
156         def send_request(self, *args, **kwargs):
157                 """
158                         Proxy connection to the send request
159                         method.
160                 """
161                 return self.core.system.send_request(*args, **kwargs)
162
163         def get_address(self, proto):
164                 """
165                         Proxy method to get the current IP address.
166                 """
167                 return self.core.system.get_address(proto)
168
169
170 class DDNSProviderDHS(DDNSProvider):
171         INFO = {
172                 "handle"    : "dhs.org",
173                 "name"      : "DHS International",
174                 "website"   : "http://dhs.org/",
175                 "protocols" : ["ipv4",]
176         }
177
178         # No information about the used update api provided on webpage,
179         # grabed from source code of ez-ipudate.
180         url = "http://members.dhs.org/nic/hosts"
181
182         def update(self):
183                 data = {
184                         "domain"       : self.hostname,
185                         "ip"           : self.get_address("ipv4"),
186                         "hostcmd"      : "edit",
187                         "hostcmdstage" : "2",
188                         "type"         : "4",
189                 }
190
191                 # Send update to the server.
192                 response = self.send_request(self.url, username=self.username, password=self.password,
193                         data=data)
194
195                 # Handle success messages.
196                 if response.code == 200:
197                         return
198
199                 # Handle error codes.
200                 elif response.code == 401:
201                         raise DDNSAuthenticationError
202
203                 # If we got here, some other update error happened.
204                 raise DDNSUpdateError
205
206
207 class DDNSProviderDNSpark(DDNSProvider):
208         INFO = {
209                 "handle"    : "dnspark.com",
210                 "name"      : "DNS Park",
211                 "website"   : "http://dnspark.com/",
212                 "protocols" : ["ipv4",]
213         }
214
215         # Informations to the used api can be found here:
216         # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
217         url = "https://control.dnspark.com/api/dynamic/update.php"
218
219         def update(self):
220                 data = {
221                         "domain" : self.hostname,
222                         "ip"     : self.get_address("ipv4"),
223                 }
224
225                 # Send update to the server.
226                 response = self.send_request(self.url, username=self.username, password=self.password,
227                         data=data)
228
229                 # Get the full response message.
230                 output = response.read()
231
232                 # Handle success messages.
233                 if output.startswith("ok") or output.startswith("nochange"):
234                         return
235
236                 # Handle error codes.
237                 if output == "unauth":
238                         raise DDNSAuthenticationError
239                 elif output == "abuse":
240                         raise DDNSAbuseError
241                 elif output == "blocked":
242                         raise DDNSBlockedError
243                 elif output == "nofqdn":
244                         raise DDNSRequestError(_("No valid FQDN was given."))
245                 elif output == "nohost":
246                         raise DDNSRequestError(_("Invalid hostname specified."))
247                 elif output == "notdyn":
248                         raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
249                 elif output == "invalid":
250                         raise DDNSRequestError(_("Invalid IP address has been sent."))
251
252                 # If we got here, some other update error happened.
253                 raise DDNSUpdateError
254
255
256 class DDNSProviderDtDNS(DDNSProvider):
257         INFO = {
258                 "handle"    : "dtdns.com",
259                 "name"      : "DtDNS",
260                 "website"   : "http://dtdns.com/",
261                 "protocols" : ["ipv4",]
262                 }
263
264         # Information about the format of the HTTPS request is to be found
265         # http://www.dtdns.com/dtsite/updatespec
266         url = "https://www.dtdns.com/api/autodns.cfm"
267
268         def update(self):
269                 data = {
270                         "ip" : self.get_address("ipv4"),
271                         "id" : self.hostname,
272                         "pw" : self.password
273                 }
274
275                 # Send update to the server.
276                 response = self.send_request(self.url, data=data)
277
278                 # Get the full response message.
279                 output = response.read()
280
281                 # Remove all leading and trailing whitespace.
282                 output = output.strip()
283
284                 # Handle success messages.
285                 if "now points to" in output:
286                         return
287
288                 # Handle error codes.
289                 if output == "No hostname to update was supplied.":
290                         raise DDNSRequestError(_("No hostname specified."))
291
292                 elif output == "The hostname you supplied is not valid.":
293                         raise DDNSRequestError(_("Invalid hostname specified."))
294
295                 elif output == "The password you supplied is not valid.":
296                         raise DDNSAuthenticationError
297
298                 elif output == "Administration has disabled this account.":
299                         raise DDNSRequestError(_("Account has been disabled."))
300
301                 elif output == "Illegal character in IP.":
302                         raise DDNSRequestError(_("Invalid IP address has been sent."))
303
304                 elif output == "Too many failed requests.":
305                         raise DDNSRequestError(_("Too many failed requests."))
306
307                 # If we got here, some other update error happened.
308                 raise DDNSUpdateError
309
310
311 class DDNSProviderDynDNS(DDNSProvider):
312         INFO = {
313                 "handle"    : "dyndns.org",
314                 "name"      : "Dyn",
315                 "website"   : "http://dyn.com/dns/",
316                 "protocols" : ["ipv4",]
317         }
318
319         # Information about the format of the request is to be found
320         # http://http://dyn.com/support/developers/api/perform-update/
321         # http://dyn.com/support/developers/api/return-codes/
322         url = "https://members.dyndns.org/nic/update"
323
324         def _prepare_request_data(self):
325                 data = {
326                         "hostname" : self.hostname,
327                         "myip"     : self.get_address("ipv4"),
328                 }
329
330                 return data
331
332         def update(self):
333                 data = self._prepare_request_data()
334
335                 # Send update to the server.
336                 response = self.send_request(self.url, data=data,
337                         username=self.username, password=self.password)
338
339                 # Get the full response message.
340                 output = response.read()
341
342                 # Handle success messages.
343                 if output.startswith("good") or output.startswith("nochg"):
344                         return
345
346                 # Handle error codes.
347                 if output == "badauth":
348                         raise DDNSAuthenticationError
349                 elif output == "aduse":
350                         raise DDNSAbuseError
351                 elif output == "notfqdn":
352                         raise DDNSRequestError(_("No valid FQDN was given."))
353                 elif output == "nohost":
354                         raise DDNSRequestError(_("Specified host does not exist."))
355                 elif output == "911":
356                         raise DDNSInternalServerError
357                 elif output == "dnserr":
358                         raise DDNSInternalServerError(_("DNS error encountered."))
359
360                 # If we got here, some other update error happened.
361                 raise DDNSUpdateError
362
363
364 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
365         INFO = {
366                 "handle"    : "freedns.afraid.org",
367                 "name"      : "freedns.afraid.org",
368                 "website"   : "http://freedns.afraid.org/",
369                 "protocols" : ["ipv6", "ipv4",]
370                 }
371
372         # No information about the request or response could be found on the vendor
373         # page. All used values have been collected by testing.
374         url = "https://freedns.afraid.org/dynamic/update.php"
375
376         @property
377         def proto(self):
378                 return self.get("proto")
379
380         def update(self):
381                 address = self.get_address(self.proto)
382
383                 data = {
384                         "address" : address,
385                 }
386
387                 # Add auth token to the update url.
388                 url = "%s?%s" % (self.url, self.token)
389
390                 # Send update to the server.
391                 response = self.send_request(url, data=data)
392
393                 if output.startswith("Updated") or "has not changed" in output:
394                         return
395
396                 # Handle error codes.
397                 if output == "ERROR: Unable to locate this record":
398                         raise DDNSAuthenticationError
399                 elif "is an invalid IP address" in output:
400                         raise DDNSRequestError(_("Invalid IP address has been sent."))
401
402
403 class DDNSProviderLightningWireLabs(DDNSProvider):
404         INFO = {
405                 "handle"    : "dns.lightningwirelabs.com",
406                 "name"      : "Lightning Wire Labs",
407                 "website"   : "http://dns.lightningwirelabs.com/",
408                 "protocols" : ["ipv6", "ipv4",]
409         }
410
411         # Information about the format of the HTTPS request is to be found
412         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
413         url = "https://dns.lightningwirelabs.com/update"
414
415         def update(self):
416                 data =  {
417                         "hostname" : self.hostname,
418                 }
419
420                 # Check if we update an IPv6 address.
421                 address6 = self.get_address("ipv6")
422                 if address6:
423                         data["address6"] = address6
424
425                 # Check if we update an IPv4 address.
426                 address4 = self.get_address("ipv4")
427                 if address4:
428                         data["address4"] = address4
429
430                 # Raise an error if none address is given.
431                 if not data.has_key("address6") and not data.has_key("address4"):
432                         raise DDNSConfigurationError
433
434                 # Check if a token has been set.
435                 if self.token:
436                         data["token"] = self.token
437
438                 # Check for username and password.
439                 elif self.username and self.password:
440                         data.update({
441                                 "username" : self.username,
442                                 "password" : self.password,
443                         })
444
445                 # Raise an error if no auth details are given.
446                 else:
447                         raise DDNSConfigurationError
448
449                 # Send update to the server.
450                 response = self.send_request(self.url, data=data)
451
452                 # Handle success messages.
453                 if response.code == 200:
454                         return
455
456                 # Handle error codes.
457                 if response.code == 403:
458                         raise DDNSAuthenticationError
459                 elif response.code == 400:
460                         raise DDNSRequestError
461
462                 # If we got here, some other update error happened.
463                 raise DDNSUpdateError
464
465
466 class DDNSProviderNOIP(DDNSProviderDynDNS):
467         INFO = {
468                 "handle"    : "no-ip.com",
469                 "name"      : "No-IP",
470                 "website"   : "http://www.no-ip.com/",
471                 "protocols" : ["ipv4",]
472         }
473
474         # Information about the format of the HTTP request is to be found
475         # here: http://www.no-ip.com/integrate/request and
476         # here: http://www.no-ip.com/integrate/response
477
478         url = "http://dynupdate.no-ip.com/nic/update"
479
480         def _prepare_request_data(self):
481                 data = {
482                         "hostname" : self.hostname,
483                         "address"  : self.get_address("ipv4"),
484                 }
485
486                 return data
487
488
489 class DDNSProviderSelfhost(DDNSProvider):
490         INFO = {
491                 "handle"    : "selfhost.de",
492                 "name"      : "Selfhost.de",
493                 "website"   : "http://www.selfhost.de/",
494                 "protocols" : ["ipv4",],
495         }
496
497         url = "https://carol.selfhost.de/update"
498
499         def update(self):
500                 data = {
501                         "username" : self.username,
502                         "password" : self.password,
503                         "textmodi" : "1",
504                 }
505
506                 response = self.send_request(self.url, data=data)
507
508                 match = re.search("status=20(0|4)", response.read())
509                 if not match:
510                         raise DDNSUpdateError
511
512
513 class DDNSProviderSPDNS(DDNSProviderDynDNS):
514         INFO = {
515                 "handle"    : "spdns.org",
516                 "name"      : "SPDNS",
517                 "website"   : "http://spdns.org/",
518                 "protocols" : ["ipv4",]
519         }
520
521         # Detailed information about request and response codes are provided
522         # by the vendor. They are using almost the same mechanism and status
523         # codes as dyndns.org so we can inherit all those stuff.
524         #
525         # http://wiki.securepoint.de/index.php/SPDNS_FAQ
526         # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
527
528         url = "https://update.spdns.de/nic/update"
529
530
531 class DDNSProviderVariomedia(DDNSProviderDynDNS):
532         INFO = {
533                 "handle"   : "variomedia.de",
534                 "name"     : "Variomedia",
535                 "website"  : "http://www.variomedia.de/",
536                 "protocols" : ["ipv6", "ipv4",]
537         }
538
539         # Detailed information about the request can be found here
540         # https://dyndns.variomedia.de/
541
542         url = "https://dyndns.variomedia.de/nic/update"
543
544         @property
545         def proto(self):
546                 return self.get("proto")
547
548         def _prepare_request_data(self):
549                 data = {
550                         "hostname" : self.hostname,
551                         "myip"     : self.get_address(self.proto)
552                 }