Various providers: properly inherit from DynDNS class.
[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         @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 DDNSProviderDynU(DDNSProviderDynDNS):
365         INFO = {
366                 "handle"    : "dynu.com",
367                 "name"      : "Dynu",
368                 "website"   : "http://dynu.com/",
369                 "protocols" : ["ipv6", "ipv4",]
370         }
371
372
373         # Detailed information about the request and response codes
374         # are available on the providers webpage.
375         # http://dynu.com/Default.aspx?page=dnsapi
376
377         url = "https://api.dynu.com/nic/update"
378
379         def _prepare_request_data(self):
380                 data = DDNSProviderDynDNS._prepare_request_data(self)
381
382                 # This one supports IPv6
383                 data.update({
384                         "myipv6"   : self.get_address("ipv6"),
385                 })
386
387                 return data
388
389
390 class DDNSProviderEasyDNS(DDNSProviderDynDNS):
391         INFO = {
392                 "handle"    : "easydns.com",
393                 "name"      : "EasyDNS",
394                 "website"   : "http://www.easydns.com/",
395                 "protocols" : ["ipv4",]
396         }
397
398         # There is only some basic documentation provided by the vendor,
399         # also searching the web gain very poor results.
400         # http://mediawiki.easydns.com/index.php/Dynamic_DNS
401
402         url = "http://api.cp.easydns.com/dyn/tomato.php"
403
404
405 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
406         INFO = {
407                 "handle"    : "freedns.afraid.org",
408                 "name"      : "freedns.afraid.org",
409                 "website"   : "http://freedns.afraid.org/",
410                 "protocols" : ["ipv6", "ipv4",]
411                 }
412
413         # No information about the request or response could be found on the vendor
414         # page. All used values have been collected by testing.
415         url = "https://freedns.afraid.org/dynamic/update.php"
416
417         @property
418         def proto(self):
419                 return self.get("proto")
420
421         def update(self):
422                 address = self.get_address(self.proto)
423
424                 data = {
425                         "address" : address,
426                 }
427
428                 # Add auth token to the update url.
429                 url = "%s?%s" % (self.url, self.token)
430
431                 # Send update to the server.
432                 response = self.send_request(url, data=data)
433
434                 if output.startswith("Updated") or "has not changed" in output:
435                         return
436
437                 # Handle error codes.
438                 if output == "ERROR: Unable to locate this record":
439                         raise DDNSAuthenticationError
440                 elif "is an invalid IP address" in output:
441                         raise DDNSRequestError(_("Invalid IP address has been sent."))
442
443
444 class DDNSProviderLightningWireLabs(DDNSProvider):
445         INFO = {
446                 "handle"    : "dns.lightningwirelabs.com",
447                 "name"      : "Lightning Wire Labs",
448                 "website"   : "http://dns.lightningwirelabs.com/",
449                 "protocols" : ["ipv6", "ipv4",]
450         }
451
452         # Information about the format of the HTTPS request is to be found
453         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
454         url = "https://dns.lightningwirelabs.com/update"
455
456         def update(self):
457                 data =  {
458                         "hostname" : self.hostname,
459                 }
460
461                 # Check if we update an IPv6 address.
462                 address6 = self.get_address("ipv6")
463                 if address6:
464                         data["address6"] = address6
465
466                 # Check if we update an IPv4 address.
467                 address4 = self.get_address("ipv4")
468                 if address4:
469                         data["address4"] = address4
470
471                 # Raise an error if none address is given.
472                 if not data.has_key("address6") and not data.has_key("address4"):
473                         raise DDNSConfigurationError
474
475                 # Check if a token has been set.
476                 if self.token:
477                         data["token"] = self.token
478
479                 # Check for username and password.
480                 elif self.username and self.password:
481                         data.update({
482                                 "username" : self.username,
483                                 "password" : self.password,
484                         })
485
486                 # Raise an error if no auth details are given.
487                 else:
488                         raise DDNSConfigurationError
489
490                 # Send update to the server.
491                 response = self.send_request(self.url, data=data)
492
493                 # Handle success messages.
494                 if response.code == 200:
495                         return
496
497                 # Handle error codes.
498                 if response.code == 403:
499                         raise DDNSAuthenticationError
500                 elif response.code == 400:
501                         raise DDNSRequestError
502
503                 # If we got here, some other update error happened.
504                 raise DDNSUpdateError
505
506
507 class DDNSProviderNOIP(DDNSProviderDynDNS):
508         INFO = {
509                 "handle"    : "no-ip.com",
510                 "name"      : "No-IP",
511                 "website"   : "http://www.no-ip.com/",
512                 "protocols" : ["ipv4",]
513         }
514
515         # Information about the format of the HTTP request is to be found
516         # here: http://www.no-ip.com/integrate/request and
517         # here: http://www.no-ip.com/integrate/response
518
519         url = "http://dynupdate.no-ip.com/nic/update"
520
521         def _prepare_request_data(self):
522                 data = {
523                         "hostname" : self.hostname,
524                         "address"  : self.get_address("ipv4"),
525                 }
526
527                 return data
528
529
530 class DDNSProviderOVH(DDNSProviderDynDNS):
531         INFO = {
532                 "handle"    : "ovh.com",
533                 "name"      : "OVH",
534                 "website"   : "http://www.ovh.com/",
535                 "protocols" : ["ipv4",]
536         }
537
538         # OVH only provides very limited information about how to
539         # update a DynDNS host. They only provide the update url
540         # on the their german subpage.
541         #
542         # http://hilfe.ovh.de/DomainDynHost
543
544         url = "https://www.ovh.com/nic/update"
545
546         def _prepare_request_data(self):
547                 data = DDNSProviderDynDNS._prepare_request_data(self)
548                 data.update({
549                         "system" : "dyndns",
550                 })
551
552                 return data
553
554
555 class DDNSProviderRegfish(DDNSProvider):
556         INFO = {
557                 "handle"    : "regfish.com",
558                 "name"      : "Regfish GmbH",
559                 "website"   : "http://www.regfish.com/",
560                 "protocols" : ["ipv6", "ipv4",]
561         }
562
563         # A full documentation to the providers api can be found here
564         # but is only available in german.
565         # https://www.regfish.de/domains/dyndns/dokumentation
566
567         url = "https://dyndns.regfish.de/"
568
569         def update(self):
570                 data = {
571                         "fqdn" : self.hostname,
572                 }
573
574                 # Check if we update an IPv6 address.
575                 address6 = self.get_address("ipv6")
576                 if address6:
577                         data["ipv6"] = address6
578
579                 # Check if we update an IPv4 address.
580                 address4 = self.get_address("ipv4")
581                 if address4:
582                         data["ipv4"] = address4
583
584                 # Raise an error if none address is given.
585                 if not data.has_key("ipv6") and not data.has_key("ipv4"):
586                         raise DDNSConfigurationError
587
588                 # Check if a token has been set.
589                 if self.token:
590                         data["token"] = self.token
591
592                 # Raise an error if no token and no useranem and password
593                 # are given.
594                 elif not self.username and not self.password:
595                         raise DDNSConfigurationError(_("No Auth details specified."))
596
597                 # HTTP Basic Auth is only allowed if no token is used.
598                 if self.token:
599                         # Send update to the server.
600                         response = self.send_request(self.url, data=data)
601                 else:
602                         # Send update to the server.
603                         response = self.send_request(self.url, username=self.username, password=self.password,
604                                 data=data)
605
606                 # Get the full response message.
607                 output = response.read()
608
609                 # Handle success messages.
610                 if "100" in output or "101" in output:
611                         return
612
613                 # Handle error codes.
614                 if "401" or "402" in output:
615                         raise DDNSAuthenticationError
616                 elif "408" in output:
617                         raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
618                 elif "409" in output:
619                         raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
620                 elif "412" in output:
621                         raise DDNSRequestError(_("No valid FQDN was given."))
622                 elif "414" in output:
623                         raise DDNSInternalServerError
624
625                 # If we got here, some other update error happened.
626                 raise DDNSUpdateError
627
628
629 class DDNSProviderSelfhost(DDNSProvider):
630         INFO = {
631                 "handle"    : "selfhost.de",
632                 "name"      : "Selfhost.de",
633                 "website"   : "http://www.selfhost.de/",
634                 "protocols" : ["ipv4",],
635         }
636
637         url = "https://carol.selfhost.de/update"
638
639         def update(self):
640                 data = {
641                         "username" : self.username,
642                         "password" : self.password,
643                         "textmodi" : "1",
644                 }
645
646                 response = self.send_request(self.url, data=data)
647
648                 match = re.search("status=20(0|4)", response.read())
649                 if not match:
650                         raise DDNSUpdateError
651
652
653 class DDNSProviderSPDNS(DDNSProviderDynDNS):
654         INFO = {
655                 "handle"    : "spdns.org",
656                 "name"      : "SPDNS",
657                 "website"   : "http://spdns.org/",
658                 "protocols" : ["ipv4",]
659         }
660
661         # Detailed information about request and response codes are provided
662         # by the vendor. They are using almost the same mechanism and status
663         # codes as dyndns.org so we can inherit all those stuff.
664         #
665         # http://wiki.securepoint.de/index.php/SPDNS_FAQ
666         # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
667
668         url = "https://update.spdns.de/nic/update"
669
670
671 class DDNSProviderVariomedia(DDNSProviderDynDNS):
672         INFO = {
673                 "handle"   : "variomedia.de",
674                 "name"     : "Variomedia",
675                 "website"  : "http://www.variomedia.de/",
676                 "protocols" : ["ipv6", "ipv4",]
677         }
678
679         # Detailed information about the request can be found here
680         # https://dyndns.variomedia.de/
681
682         url = "https://dyndns.variomedia.de/nic/update"
683
684         @property
685         def proto(self):
686                 return self.get("proto")
687
688         def _prepare_request_data(self):
689                 data = {
690                         "hostname" : self.hostname,
691                         "myip"     : self.get_address(self.proto)
692                 }
693
694                 return data