Add empty lines between api documentation and the update url.
[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 import urllib2
24 import xml.dom.minidom
25
26 from i18n import _
27
28 # Import all possible exception types.
29 from .errors import *
30
31 logger = logging.getLogger("ddns.providers")
32 logger.propagate = 1
33
34 class DDNSProvider(object):
35         # A short string that uniquely identifies
36         # this provider.
37         handle = None
38
39         # The full name of the provider.
40         name = None
41
42         # A weburl to the homepage of the provider.
43         # (Where to register a new account?)
44         website = None
45
46         # A list of supported protocols.
47         protocols = ("ipv6", "ipv4")
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         def get(self, key, default=None):
66                 """
67                         Get a setting from the settings dictionary.
68                 """
69                 return self.settings.get(key, default)
70
71         @property
72         def hostname(self):
73                 """
74                         Fast access to the hostname.
75                 """
76                 return self.get("hostname")
77
78         @property
79         def username(self):
80                 """
81                         Fast access to the username.
82                 """
83                 return self.get("username")
84
85         @property
86         def password(self):
87                 """
88                         Fast access to the password.
89                 """
90                 return self.get("password")
91
92         @property
93         def token(self):
94                 """
95                         Fast access to the token.
96                 """
97                 return self.get("token")
98
99         def __call__(self, force=False):
100                 if force:
101                         logger.debug(_("Updating %s forced") % self.hostname)
102
103                 # Check if we actually need to update this host.
104                 elif self.is_uptodate(self.protocols):
105                         logger.debug(_("%s is already up to date") % self.hostname)
106                         return
107
108                 # Execute the update.
109                 self.update()
110
111         def update(self):
112                 raise NotImplementedError
113
114         def is_uptodate(self, protos):
115                 """
116                         Returns True if this host is already up to date
117                         and does not need to change the IP address on the
118                         name server.
119                 """
120                 for proto in protos:
121                         addresses = self.core.system.resolve(self.hostname, proto)
122
123                         current_address = self.get_address(proto)
124
125                         # If no addresses for the given protocol exist, we
126                         # are fine...
127                         if current_address is None and not addresses:
128                                 continue
129
130                         if not current_address in addresses:
131                                 return False
132
133                 return True
134
135         def send_request(self, *args, **kwargs):
136                 """
137                         Proxy connection to the send request
138                         method.
139                 """
140                 return self.core.system.send_request(*args, **kwargs)
141
142         def get_address(self, proto, default=None):
143                 """
144                         Proxy method to get the current IP address.
145                 """
146                 return self.core.system.get_address(proto) or default
147
148
149 class DDNSProviderAllInkl(DDNSProvider):
150         handle    = "all-inkl.com"
151         name      = "All-inkl.com"
152         website   = "http://all-inkl.com/"
153         protocols = ("ipv4",)
154
155         # There are only information provided by the vendor how to
156         # perform an update on a FRITZ Box. Grab requried informations
157         # from the net.
158         # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
159
160         url = "http://dyndns.kasserver.com"
161
162         def update(self):
163                 # There is no additional data required so we directly can
164                 # send our request.
165                 response = self.send_request(self.url, username=self.username, password=self.password)
166
167                 # Get the full response message.
168                 output = response.read()
169
170                 # Handle success messages.
171                 if output.startswith("good") or output.startswith("nochg"):
172                         return
173
174                 # If we got here, some other update error happened.
175                 raise DDNSUpdateError
176
177
178 class DDNSProviderDHS(DDNSProvider):
179         handle    = "dhs.org"
180         name      = "DHS International"
181         website   = "http://dhs.org/"
182         protocols = ("ipv4",)
183
184         # No information about the used update api provided on webpage,
185         # grabed from source code of ez-ipudate.
186
187         url = "http://members.dhs.org/nic/hosts"
188
189         def update(self):
190                 data = {
191                         "domain"       : self.hostname,
192                         "ip"           : self.get_address("ipv4"),
193                         "hostcmd"      : "edit",
194                         "hostcmdstage" : "2",
195                         "type"         : "4",
196                 }
197
198                 # Send update to the server.
199                 response = self.send_request(self.url, username=self.username, password=self.password,
200                         data=data)
201
202                 # Handle success messages.
203                 if response.code == 200:
204                         return
205
206                 # If we got here, some other update error happened.
207                 raise DDNSUpdateError
208
209
210 class DDNSProviderDNSpark(DDNSProvider):
211         handle    = "dnspark.com"
212         name      = "DNS Park"
213         website   = "http://dnspark.com/"
214         protocols = ("ipv4",)
215
216         # Informations to the used api can be found here:
217         # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
218
219         url = "https://control.dnspark.com/api/dynamic/update.php"
220
221         def update(self):
222                 data = {
223                         "domain" : self.hostname,
224                         "ip"     : self.get_address("ipv4"),
225                 }
226
227                 # Send update to the server.
228                 response = self.send_request(self.url, username=self.username, password=self.password,
229                         data=data)
230
231                 # Get the full response message.
232                 output = response.read()
233
234                 # Handle success messages.
235                 if output.startswith("ok") or output.startswith("nochange"):
236                         return
237
238                 # Handle error codes.
239                 if output == "unauth":
240                         raise DDNSAuthenticationError
241                 elif output == "abuse":
242                         raise DDNSAbuseError
243                 elif output == "blocked":
244                         raise DDNSBlockedError
245                 elif output == "nofqdn":
246                         raise DDNSRequestError(_("No valid FQDN was given."))
247                 elif output == "nohost":
248                         raise DDNSRequestError(_("Invalid hostname specified."))
249                 elif output == "notdyn":
250                         raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
251                 elif output == "invalid":
252                         raise DDNSRequestError(_("Invalid IP address has been sent."))
253
254                 # If we got here, some other update error happened.
255                 raise DDNSUpdateError
256
257
258 class DDNSProviderDtDNS(DDNSProvider):
259         handle    = "dtdns.com"
260         name      = "DtDNS"
261         website   = "http://dtdns.com/"
262         protocols = ("ipv4",)
263
264         # Information about the format of the HTTPS request is to be found
265         # http://www.dtdns.com/dtsite/updatespec
266
267         url = "https://www.dtdns.com/api/autodns.cfm"
268
269         def update(self):
270                 data = {
271                         "ip" : self.get_address("ipv4"),
272                         "id" : self.hostname,
273                         "pw" : self.password
274                 }
275
276                 # Send update to the server.
277                 response = self.send_request(self.url, data=data)
278
279                 # Get the full response message.
280                 output = response.read()
281
282                 # Remove all leading and trailing whitespace.
283                 output = output.strip()
284
285                 # Handle success messages.
286                 if "now points to" in output:
287                         return
288
289                 # Handle error codes.
290                 if output == "No hostname to update was supplied.":
291                         raise DDNSRequestError(_("No hostname specified."))
292
293                 elif output == "The hostname you supplied is not valid.":
294                         raise DDNSRequestError(_("Invalid hostname specified."))
295
296                 elif output == "The password you supplied is not valid.":
297                         raise DDNSAuthenticationError
298
299                 elif output == "Administration has disabled this account.":
300                         raise DDNSRequestError(_("Account has been disabled."))
301
302                 elif output == "Illegal character in IP.":
303                         raise DDNSRequestError(_("Invalid IP address has been sent."))
304
305                 elif output == "Too many failed requests.":
306                         raise DDNSRequestError(_("Too many failed requests."))
307
308                 # If we got here, some other update error happened.
309                 raise DDNSUpdateError
310
311
312 class DDNSProviderDynDNS(DDNSProvider):
313         handle    = "dyndns.org"
314         name      = "Dyn"
315         website   = "http://dyn.com/dns/"
316         protocols = ("ipv4",)
317
318         # Information about the format of the request is to be found
319         # http://http://dyn.com/support/developers/api/perform-update/
320         # http://dyn.com/support/developers/api/return-codes/
321
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(_("Server response: %s") % output)
362
363
364 class DDNSProviderDynU(DDNSProviderDynDNS):
365         handle    = "dynu.com"
366         name      = "Dynu"
367         website   = "http://dynu.com/"
368         protocols = ("ipv6", "ipv4",)
369
370         # Detailed information about the request and response codes
371         # are available on the providers webpage.
372         # http://dynu.com/Default.aspx?page=dnsapi
373
374         url = "https://api.dynu.com/nic/update"
375
376         def _prepare_request_data(self):
377                 data = DDNSProviderDynDNS._prepare_request_data(self)
378
379                 # This one supports IPv6
380                 data.update({
381                         "myipv6"   : self.get_address("ipv6"),
382                 })
383
384                 return data
385
386
387 class DDNSProviderEasyDNS(DDNSProviderDynDNS):
388         handle  = "easydns.com"
389         name    = "EasyDNS"
390         website = "http://www.easydns.com/"
391
392         # There is only some basic documentation provided by the vendor,
393         # also searching the web gain very poor results.
394         # http://mediawiki.easydns.com/index.php/Dynamic_DNS
395
396         url = "http://api.cp.easydns.com/dyn/tomato.php"
397
398
399 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
400         handle    = "freedns.afraid.org"
401         name      = "freedns.afraid.org"
402         website   = "http://freedns.afraid.org/"
403
404         # No information about the request or response could be found on the vendor
405         # page. All used values have been collected by testing.
406         url = "https://freedns.afraid.org/dynamic/update.php"
407
408         @property
409         def proto(self):
410                 return self.get("proto")
411
412         def update(self):
413                 address = self.get_address(self.proto)
414
415                 data = {
416                         "address" : address,
417                 }
418
419                 # Add auth token to the update url.
420                 url = "%s?%s" % (self.url, self.token)
421
422                 # Send update to the server.
423                 response = self.send_request(url, data=data)
424
425                 if output.startswith("Updated") or "has not changed" in output:
426                         return
427
428                 # Handle error codes.
429                 if output == "ERROR: Unable to locate this record":
430                         raise DDNSAuthenticationError
431                 elif "is an invalid IP address" in output:
432                         raise DDNSRequestError(_("Invalid IP address has been sent."))
433
434                 # If we got here, some other update error happened.
435                 raise DDNSUpdateError
436
437
438 class DDNSProviderLightningWireLabs(DDNSProvider):
439         handle    = "dns.lightningwirelabs.com"
440         name      = "Lightning Wire Labs DNS Service"
441         website   = "http://dns.lightningwirelabs.com/"
442
443         # Information about the format of the HTTPS request is to be found
444         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
445
446         url = "https://dns.lightningwirelabs.com/update"
447
448         def update(self):
449                 data =  {
450                         "hostname" : self.hostname,
451                         "address6" : self.get_address("ipv6", "-"),
452                         "address4" : self.get_address("ipv4", "-"),
453                 }
454
455                 # Check if a token has been set.
456                 if self.token:
457                         data["token"] = self.token
458
459                 # Check for username and password.
460                 elif self.username and self.password:
461                         data.update({
462                                 "username" : self.username,
463                                 "password" : self.password,
464                         })
465
466                 # Raise an error if no auth details are given.
467                 else:
468                         raise DDNSConfigurationError
469
470                 # Send update to the server.
471                 response = self.send_request(self.url, data=data)
472
473                 # Handle success messages.
474                 if response.code == 200:
475                         return
476
477                 # If we got here, some other update error happened.
478                 raise DDNSUpdateError
479
480
481 class DDNSProviderNamecheap(DDNSProvider):
482         handle    = "namecheap.com"
483         name      = "Namecheap"
484         website   = "http://namecheap.com"
485         protocols = ("ipv4",)
486
487         # Information about the format of the HTTP request is to be found
488         # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
489         # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
490
491         url = "https://dynamicdns.park-your-domain.com/update"
492
493         def parse_xml(self, document, content):
494                 # Send input to the parser.
495                 xmldoc = xml.dom.minidom.parseString(document)
496
497                 # Get XML elements by the given content.
498                 element = xmldoc.getElementsByTagName(content)
499
500                 # If no element has been found, we directly can return None.
501                 if not element:
502                         return None
503
504                 # Only get the first child from an element, even there are more than one.
505                 firstchild = element[0].firstChild
506
507                 # Get the value of the child.
508                 value = firstchild.nodeValue
509
510                 # Return the value.
511                 return value
512                 
513         def update(self):
514                 # Namecheap requires the hostname splitted into a host and domain part.
515                 host, domain = self.hostname.split(".", 1)
516
517                 data = {
518                         "ip"       : self.get_address("ipv4"),
519                         "password" : self.password,
520                         "host"     : host,
521                         "domain"   : domain
522                 }
523
524                 # Send update to the server.
525                 response = self.send_request(self.url, data=data)
526
527                 # Get the full response message.
528                 output = response.read()
529
530                 # Handle success messages.
531                 if self.parse_xml(output, "IP") == self.get_address("ipv4"):
532                         return
533
534                 # Handle error codes.
535                 errorcode = self.parse_xml(output, "ResponseNumber")
536
537                 if errorcode == "304156":
538                         raise DDNSAuthenticationError
539                 elif errorcode == "316153":
540                         raise DDNSRequestError(_("Domain not found."))
541                 elif errorcode == "316154":
542                         raise DDNSRequestError(_("Domain not active."))
543                 elif errorcode in ("380098", "380099"):
544                         raise DDNSInternalServerError
545
546                 # If we got here, some other update error happened.
547                 raise DDNSUpdateError
548
549
550 class DDNSProviderNOIP(DDNSProviderDynDNS):
551         handle  = "no-ip.com"
552         name    = "No-IP"
553         website = "http://www.no-ip.com/"
554
555         # Information about the format of the HTTP request is to be found
556         # here: http://www.no-ip.com/integrate/request and
557         # here: http://www.no-ip.com/integrate/response
558
559         url = "http://dynupdate.no-ip.com/nic/update"
560
561         def _prepare_request_data(self):
562                 data = {
563                         "hostname" : self.hostname,
564                         "address"  : self.get_address("ipv4"),
565                 }
566
567                 return data
568
569
570 class DDNSProviderOVH(DDNSProviderDynDNS):
571         handle  = "ovh.com"
572         name    = "OVH"
573         website = "http://www.ovh.com/"
574
575         # OVH only provides very limited information about how to
576         # update a DynDNS host. They only provide the update url
577         # on the their german subpage.
578         #
579         # http://hilfe.ovh.de/DomainDynHost
580
581         url = "https://www.ovh.com/nic/update"
582
583         def _prepare_request_data(self):
584                 data = DDNSProviderDynDNS._prepare_request_data(self)
585                 data.update({
586                         "system" : "dyndns",
587                 })
588
589                 return data
590
591
592 class DDNSProviderRegfish(DDNSProvider):
593         handle  = "regfish.com"
594         name    = "Regfish GmbH"
595         website = "http://www.regfish.com/"
596
597         # A full documentation to the providers api can be found here
598         # but is only available in german.
599         # https://www.regfish.de/domains/dyndns/dokumentation
600
601         url = "https://dyndns.regfish.de/"
602
603         def update(self):
604                 data = {
605                         "fqdn" : self.hostname,
606                 }
607
608                 # Check if we update an IPv6 address.
609                 address6 = self.get_address("ipv6")
610                 if address6:
611                         data["ipv6"] = address6
612
613                 # Check if we update an IPv4 address.
614                 address4 = self.get_address("ipv4")
615                 if address4:
616                         data["ipv4"] = address4
617
618                 # Raise an error if none address is given.
619                 if not data.has_key("ipv6") and not data.has_key("ipv4"):
620                         raise DDNSConfigurationError
621
622                 # Check if a token has been set.
623                 if self.token:
624                         data["token"] = self.token
625
626                 # Raise an error if no token and no useranem and password
627                 # are given.
628                 elif not self.username and not self.password:
629                         raise DDNSConfigurationError(_("No Auth details specified."))
630
631                 # HTTP Basic Auth is only allowed if no token is used.
632                 if self.token:
633                         # Send update to the server.
634                         response = self.send_request(self.url, data=data)
635                 else:
636                         # Send update to the server.
637                         response = self.send_request(self.url, username=self.username, password=self.password,
638                                 data=data)
639
640                 # Get the full response message.
641                 output = response.read()
642
643                 # Handle success messages.
644                 if "100" in output or "101" in output:
645                         return
646
647                 # Handle error codes.
648                 if "401" or "402" in output:
649                         raise DDNSAuthenticationError
650                 elif "408" in output:
651                         raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
652                 elif "409" in output:
653                         raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
654                 elif "412" in output:
655                         raise DDNSRequestError(_("No valid FQDN was given."))
656                 elif "414" in output:
657                         raise DDNSInternalServerError
658
659                 # If we got here, some other update error happened.
660                 raise DDNSUpdateError
661
662
663 class DDNSProviderSelfhost(DDNSProviderDynDNS):
664         handle    = "selfhost.de"
665         name      = "Selfhost.de"
666         website   = "http://www.selfhost.de/"
667
668         url = "https://carol.selfhost.de/nic/update"
669
670         def _prepare_request_data(self):
671                 data = DDNSProviderDynDNS._prepare_request_data(self)
672                 data.update({
673                         "hostname" : "1",
674                 })
675
676                 return data
677
678
679 class DDNSProviderSPDNS(DDNSProviderDynDNS):
680         handle  = "spdns.org"
681         name    = "SPDNS"
682         website = "http://spdns.org/"
683
684         # Detailed information about request and response codes are provided
685         # by the vendor. They are using almost the same mechanism and status
686         # codes as dyndns.org so we can inherit all those stuff.
687         #
688         # http://wiki.securepoint.de/index.php/SPDNS_FAQ
689         # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
690
691         url = "https://update.spdns.de/nic/update"
692
693
694 class DDNSProviderStrato(DDNSProviderDynDNS):
695         handle  = "strato.com"
696         name    = "Strato AG"
697         website = "http:/www.strato.com/"
698
699         # Information about the request and response can be obtained here:
700         # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
701
702         url = "https://dyndns.strato.com/nic/update"
703
704
705 class DDNSProviderTwoDNS(DDNSProviderDynDNS):
706         handle  = "twodns.de"
707         name    = "TwoDNS"
708         website = "http://www.twodns.de"
709
710         # Detailed information about the request can be found here
711         # http://twodns.de/en/faqs
712         # http://twodns.de/en/api
713
714         url = "https://update.twodns.de/update"
715
716         def _prepare_request_data(self):
717                 data = {
718                         "ip" : self.get_address("ipv4"),
719                         "hostname" : self.hostname
720                 }
721
722                 return data
723
724
725 class DDNSProviderUdmedia(DDNSProviderDynDNS):
726         handle  = "udmedia.de"
727         name    = "Udmedia GmbH"
728         website = "http://www.udmedia.de"
729
730         # Information about the request can be found here
731         # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
732
733         url = "https://www.udmedia.de/nic/update"
734
735
736 class DDNSProviderVariomedia(DDNSProviderDynDNS):
737         handle    = "variomedia.de"
738         name      = "Variomedia"
739         website   = "http://www.variomedia.de/"
740         protocols = ("ipv6", "ipv4",)
741
742         # Detailed information about the request can be found here
743         # https://dyndns.variomedia.de/
744
745         url = "https://dyndns.variomedia.de/nic/update"
746
747         @property
748         def proto(self):
749                 return self.get("proto")
750
751         def _prepare_request_data(self):
752                 data = {
753                         "hostname" : self.hostname,
754                         "myip"     : self.get_address(self.proto)
755                 }
756
757                 return data
758
759
760 class DDNSProviderZoneedit(DDNSProvider):
761         handle  = "zoneedit.com"
762         name    = "Zoneedit"
763         website = "http://www.zoneedit.com"
764
765         # Detailed information about the request and the response codes can be
766         # obtained here:
767         # http://www.zoneedit.com/doc/api/other.html
768         # http://www.zoneedit.com/faq.html
769
770         url = "https://dynamic.zoneedit.com/auth/dynamic.html"
771
772         @property
773         def proto(self):
774                 return self.get("proto")
775
776         def update(self):
777                 data = {
778                         "dnsto" : self.get_address(self.proto),
779                         "host"  : self.hostname
780                 }
781
782                 # Send update to the server.
783                 response = self.send_request(self.url, username=self.username, password=self.password,
784                         data=data)
785
786                 # Get the full response message.
787                 output = response.read()
788
789                 # Handle success messages.
790                 if output.startswith("<SUCCESS"):
791                         return
792
793                 # Handle error codes.
794                 if output.startswith("invalid login"):
795                         raise DDNSAuthenticationError
796                 elif output.startswith("<ERROR CODE=\"704\""):
797                         raise DDNSRequestError(_("No valid FQDN was given.")) 
798                 elif output.startswith("<ERROR CODE=\"702\""):
799                         raise DDNSInternalServerError
800
801                 # If we got here, some other update error happened.
802                 raise DDNSUpdateError