freedns.afraid.com: Read accidently removed exeption.
[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 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         url = "http://members.dhs.org/nic/hosts"
187
188         def update(self):
189                 data = {
190                         "domain"       : self.hostname,
191                         "ip"           : self.get_address("ipv4"),
192                         "hostcmd"      : "edit",
193                         "hostcmdstage" : "2",
194                         "type"         : "4",
195                 }
196
197                 # Send update to the server.
198                 response = self.send_request(self.url, username=self.username, password=self.password,
199                         data=data)
200
201                 # Handle success messages.
202                 if response.code == 200:
203                         return
204
205                 # If we got here, some other update error happened.
206                 raise DDNSUpdateError
207
208
209 class DDNSProviderDNSpark(DDNSProvider):
210         handle    = "dnspark.com"
211         name      = "DNS Park"
212         website   = "http://dnspark.com/"
213         protocols = ("ipv4",)
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         handle    = "dtdns.com"
258         name      = "DtDNS"
259         website   = "http://dtdns.com/"
260         protocols = ("ipv4",)
261
262         # Information about the format of the HTTPS request is to be found
263         # http://www.dtdns.com/dtsite/updatespec
264         url = "https://www.dtdns.com/api/autodns.cfm"
265
266         def update(self):
267                 data = {
268                         "ip" : self.get_address("ipv4"),
269                         "id" : self.hostname,
270                         "pw" : self.password
271                 }
272
273                 # Send update to the server.
274                 response = self.send_request(self.url, data=data)
275
276                 # Get the full response message.
277                 output = response.read()
278
279                 # Remove all leading and trailing whitespace.
280                 output = output.strip()
281
282                 # Handle success messages.
283                 if "now points to" in output:
284                         return
285
286                 # Handle error codes.
287                 if output == "No hostname to update was supplied.":
288                         raise DDNSRequestError(_("No hostname specified."))
289
290                 elif output == "The hostname you supplied is not valid.":
291                         raise DDNSRequestError(_("Invalid hostname specified."))
292
293                 elif output == "The password you supplied is not valid.":
294                         raise DDNSAuthenticationError
295
296                 elif output == "Administration has disabled this account.":
297                         raise DDNSRequestError(_("Account has been disabled."))
298
299                 elif output == "Illegal character in IP.":
300                         raise DDNSRequestError(_("Invalid IP address has been sent."))
301
302                 elif output == "Too many failed requests.":
303                         raise DDNSRequestError(_("Too many failed requests."))
304
305                 # If we got here, some other update error happened.
306                 raise DDNSUpdateError
307
308
309 class DDNSProviderDynDNS(DDNSProvider):
310         handle    = "dyndns.org"
311         name      = "Dyn"
312         website   = "http://dyn.com/dns/"
313         protocols = ("ipv4",)
314
315         # Information about the format of the request is to be found
316         # http://http://dyn.com/support/developers/api/perform-update/
317         # http://dyn.com/support/developers/api/return-codes/
318         url = "https://members.dyndns.org/nic/update"
319
320         def _prepare_request_data(self):
321                 data = {
322                         "hostname" : self.hostname,
323                         "myip"     : self.get_address("ipv4"),
324                 }
325
326                 return data
327
328         def update(self):
329                 data = self._prepare_request_data()
330
331                 # Send update to the server.
332                 response = self.send_request(self.url, data=data,
333                         username=self.username, password=self.password)
334
335                 # Get the full response message.
336                 output = response.read()
337
338                 # Handle success messages.
339                 if output.startswith("good") or output.startswith("nochg"):
340                         return
341
342                 # Handle error codes.
343                 if output == "badauth":
344                         raise DDNSAuthenticationError
345                 elif output == "aduse":
346                         raise DDNSAbuseError
347                 elif output == "notfqdn":
348                         raise DDNSRequestError(_("No valid FQDN was given."))
349                 elif output == "nohost":
350                         raise DDNSRequestError(_("Specified host does not exist."))
351                 elif output == "911":
352                         raise DDNSInternalServerError
353                 elif output == "dnserr":
354                         raise DDNSInternalServerError(_("DNS error encountered."))
355
356                 # If we got here, some other update error happened.
357                 raise DDNSUpdateError(_("Server response: %s") % output)
358
359
360 class DDNSProviderDynU(DDNSProviderDynDNS):
361         handle    = "dynu.com"
362         name      = "Dynu"
363         website   = "http://dynu.com/"
364         protocols = ("ipv6", "ipv4",)
365
366         # Detailed information about the request and response codes
367         # are available on the providers webpage.
368         # http://dynu.com/Default.aspx?page=dnsapi
369
370         url = "https://api.dynu.com/nic/update"
371
372         def _prepare_request_data(self):
373                 data = DDNSProviderDynDNS._prepare_request_data(self)
374
375                 # This one supports IPv6
376                 data.update({
377                         "myipv6"   : self.get_address("ipv6"),
378                 })
379
380                 return data
381
382
383 class DDNSProviderEasyDNS(DDNSProviderDynDNS):
384         handle  = "easydns.com"
385         name    = "EasyDNS"
386         website = "http://www.easydns.com/"
387
388         # There is only some basic documentation provided by the vendor,
389         # also searching the web gain very poor results.
390         # http://mediawiki.easydns.com/index.php/Dynamic_DNS
391
392         url = "http://api.cp.easydns.com/dyn/tomato.php"
393
394
395 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
396         handle    = "freedns.afraid.org"
397         name      = "freedns.afraid.org"
398         website   = "http://freedns.afraid.org/"
399
400         # No information about the request or response could be found on the vendor
401         # page. All used values have been collected by testing.
402         url = "https://freedns.afraid.org/dynamic/update.php"
403
404         @property
405         def proto(self):
406                 return self.get("proto")
407
408         def update(self):
409                 address = self.get_address(self.proto)
410
411                 data = {
412                         "address" : address,
413                 }
414
415                 # Add auth token to the update url.
416                 url = "%s?%s" % (self.url, self.token)
417
418                 # Send update to the server.
419                 response = self.send_request(url, data=data)
420
421                 if output.startswith("Updated") or "has not changed" in output:
422                         return
423
424                 # Handle error codes.
425                 if output == "ERROR: Unable to locate this record":
426                         raise DDNSAuthenticationError
427                 elif "is an invalid IP address" in output:
428                         raise DDNSRequestError(_("Invalid IP address has been sent."))
429
430                 # If we got here, some other update error happened.
431                 raise DDNSUpdateError
432
433
434 class DDNSProviderLightningWireLabs(DDNSProvider):
435         handle    = "dns.lightningwirelabs.com"
436         name      = "Lightning Wire Labs DNS Service"
437         website   = "http://dns.lightningwirelabs.com/"
438
439         # Information about the format of the HTTPS request is to be found
440         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
441         url = "https://dns.lightningwirelabs.com/update"
442
443         def update(self):
444                 data =  {
445                         "hostname" : self.hostname,
446                         "address6" : self.get_address("ipv6", "-"),
447                         "address4" : self.get_address("ipv4", "-"),
448                 }
449
450                 # Check if a token has been set.
451                 if self.token:
452                         data["token"] = self.token
453
454                 # Check for username and password.
455                 elif self.username and self.password:
456                         data.update({
457                                 "username" : self.username,
458                                 "password" : self.password,
459                         })
460
461                 # Raise an error if no auth details are given.
462                 else:
463                         raise DDNSConfigurationError
464
465                 # Send update to the server.
466                 response = self.send_request(self.url, data=data)
467
468                 # Handle success messages.
469                 if response.code == 200:
470                         return
471
472                 # If we got here, some other update error happened.
473                 raise DDNSUpdateError
474
475
476 class DDNSProviderNamecheap(DDNSProvider):
477         handle    = "namecheap.com"
478         name      = "Namecheap"
479         website   = "http://namecheap.com"
480         protocols = ("ipv4",)
481
482         # Information about the format of the HTTP request is to be found
483         # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
484         # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
485
486         url = "https://dynamicdns.park-your-domain.com/update"
487
488         def parse_xml(self, document, content):
489                 # Send input to the parser.
490                 xmldoc = xml.dom.minidom.parseString(document)
491
492                 # Get XML elements by the given content.
493                 element = xmldoc.getElementsByTagName(content)
494
495                 # If no element has been found, we directly can return None.
496                 if not element:
497                         return None
498
499                 # Only get the first child from an element, even there are more than one.
500                 firstchild = element[0].firstChild
501
502                 # Get the value of the child.
503                 value = firstchild.nodeValue
504
505                 # Return the value.
506                 return value
507                 
508         def update(self):
509                 # Namecheap requires the hostname splitted into a host and domain part.
510                 host, domain = self.hostname.split(".", 1)
511
512                 data = {
513                         "ip"       : self.get_address("ipv4"),
514                         "password" : self.password,
515                         "host"     : host,
516                         "domain"   : domain
517                 }
518
519                 # Send update to the server.
520                 response = self.send_request(self.url, data=data)
521
522                 # Get the full response message.
523                 output = response.read()
524
525                 # Handle success messages.
526                 if self.parse_xml(output, "IP") == self.get_address("ipv4"):
527                         return
528
529                 # Handle error codes.
530                 errorcode = self.parse_xml(output, "ResponseNumber")
531
532                 if errorcode == "304156":
533                         raise DDNSAuthenticationError
534                 elif errorcode == "316153":
535                         raise DDNSRequestError(_("Domain not found."))
536                 elif errorcode == "316154":
537                         raise DDNSRequestError(_("Domain not active."))
538                 elif errorcode in ("380098", "380099"):
539                         raise DDNSInternalServerError
540
541                 # If we got here, some other update error happened.
542                 raise DDNSUpdateError
543
544
545 class DDNSProviderNOIP(DDNSProviderDynDNS):
546         handle  = "no-ip.com"
547         name    = "No-IP"
548         website = "http://www.no-ip.com/"
549
550         # Information about the format of the HTTP request is to be found
551         # here: http://www.no-ip.com/integrate/request and
552         # here: http://www.no-ip.com/integrate/response
553
554         url = "http://dynupdate.no-ip.com/nic/update"
555
556         def _prepare_request_data(self):
557                 data = {
558                         "hostname" : self.hostname,
559                         "address"  : self.get_address("ipv4"),
560                 }
561
562                 return data
563
564
565 class DDNSProviderOVH(DDNSProviderDynDNS):
566         handle  = "ovh.com"
567         name    = "OVH"
568         website = "http://www.ovh.com/"
569
570         # OVH only provides very limited information about how to
571         # update a DynDNS host. They only provide the update url
572         # on the their german subpage.
573         #
574         # http://hilfe.ovh.de/DomainDynHost
575
576         url = "https://www.ovh.com/nic/update"
577
578         def _prepare_request_data(self):
579                 data = DDNSProviderDynDNS._prepare_request_data(self)
580                 data.update({
581                         "system" : "dyndns",
582                 })
583
584                 return data
585
586
587 class DDNSProviderRegfish(DDNSProvider):
588         handle  = "regfish.com"
589         name    = "Regfish GmbH"
590         website = "http://www.regfish.com/"
591
592         # A full documentation to the providers api can be found here
593         # but is only available in german.
594         # https://www.regfish.de/domains/dyndns/dokumentation
595
596         url = "https://dyndns.regfish.de/"
597
598         def update(self):
599                 data = {
600                         "fqdn" : self.hostname,
601                 }
602
603                 # Check if we update an IPv6 address.
604                 address6 = self.get_address("ipv6")
605                 if address6:
606                         data["ipv6"] = address6
607
608                 # Check if we update an IPv4 address.
609                 address4 = self.get_address("ipv4")
610                 if address4:
611                         data["ipv4"] = address4
612
613                 # Raise an error if none address is given.
614                 if not data.has_key("ipv6") and not data.has_key("ipv4"):
615                         raise DDNSConfigurationError
616
617                 # Check if a token has been set.
618                 if self.token:
619                         data["token"] = self.token
620
621                 # Raise an error if no token and no useranem and password
622                 # are given.
623                 elif not self.username and not self.password:
624                         raise DDNSConfigurationError(_("No Auth details specified."))
625
626                 # HTTP Basic Auth is only allowed if no token is used.
627                 if self.token:
628                         # Send update to the server.
629                         response = self.send_request(self.url, data=data)
630                 else:
631                         # Send update to the server.
632                         response = self.send_request(self.url, username=self.username, password=self.password,
633                                 data=data)
634
635                 # Get the full response message.
636                 output = response.read()
637
638                 # Handle success messages.
639                 if "100" in output or "101" in output:
640                         return
641
642                 # Handle error codes.
643                 if "401" or "402" in output:
644                         raise DDNSAuthenticationError
645                 elif "408" in output:
646                         raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
647                 elif "409" in output:
648                         raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
649                 elif "412" in output:
650                         raise DDNSRequestError(_("No valid FQDN was given."))
651                 elif "414" in output:
652                         raise DDNSInternalServerError
653
654                 # If we got here, some other update error happened.
655                 raise DDNSUpdateError
656
657
658 class DDNSProviderSelfhost(DDNSProviderDynDNS):
659         handle    = "selfhost.de"
660         name      = "Selfhost.de"
661         website   = "http://www.selfhost.de/"
662
663         url = "https://carol.selfhost.de/nic/update"
664
665         def _prepare_request_data(self):
666                 data = DDNSProviderDynDNS._prepare_request_data(self)
667                 data.update({
668                         "hostname" : "1",
669                 })
670
671                 return data
672
673
674 class DDNSProviderSPDNS(DDNSProviderDynDNS):
675         handle  = "spdns.org"
676         name    = "SPDNS"
677         website = "http://spdns.org/"
678
679         # Detailed information about request and response codes are provided
680         # by the vendor. They are using almost the same mechanism and status
681         # codes as dyndns.org so we can inherit all those stuff.
682         #
683         # http://wiki.securepoint.de/index.php/SPDNS_FAQ
684         # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
685
686         url = "https://update.spdns.de/nic/update"
687
688
689 class DDNSProviderStrato(DDNSProviderDynDNS):
690         handle  = "strato.com"
691         name    = "Strato AG"
692         website = "http:/www.strato.com/"
693
694         # Information about the request and response can be obtained here:
695         # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
696
697         url = "https://dyndns.strato.com/nic/update"
698
699
700 class DDNSProviderTwoDNS(DDNSProviderDynDNS):
701         handle  = "twodns.de"
702         name    = "TwoDNS"
703         website = "http://www.twodns.de"
704
705         # Detailed information about the request can be found here
706         # http://twodns.de/en/faqs
707         # http://twodns.de/en/api
708
709         url = "https://update.twodns.de/update"
710
711         def _prepare_request_data(self):
712                 data = {
713                         "ip" : self.get_address("ipv4"),
714                         "hostname" : self.hostname
715                 }
716
717                 return data
718
719
720 class DDNSProviderUdmedia(DDNSProviderDynDNS):
721         handle  = "udmedia.de"
722         name    = "Udmedia GmbH"
723         website = "http://www.udmedia.de"
724
725         # Information about the request can be found here
726         # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
727
728         url = "https://www.udmedia.de/nic/update"
729
730
731 class DDNSProviderVariomedia(DDNSProviderDynDNS):
732         handle    = "variomedia.de"
733         name      = "Variomedia"
734         website   = "http://www.variomedia.de/"
735         protocols = ("ipv6", "ipv4",)
736
737         # Detailed information about the request can be found here
738         # https://dyndns.variomedia.de/
739
740         url = "https://dyndns.variomedia.de/nic/update"
741
742         @property
743         def proto(self):
744                 return self.get("proto")
745
746         def _prepare_request_data(self):
747                 data = {
748                         "hostname" : self.hostname,
749                         "myip"     : self.get_address(self.proto)
750                 }
751
752                 return data
753
754
755 class DDNSProviderZoneedit(DDNSProvider):
756         handle  = "zoneedit.com"
757         name    = "Zoneedit"
758         website = "http://www.zoneedit.com"
759
760         # Detailed information about the request and the response codes can be
761         # obtained here:
762         # http://www.zoneedit.com/doc/api/other.html
763         # http://www.zoneedit.com/faq.html
764
765         url = "https://dynamic.zoneedit.com/auth/dynamic.html"
766
767         @property
768         def proto(self):
769                 return self.get("proto")
770
771         def update(self):
772                 data = {
773                         "dnsto" : self.get_address(self.proto),
774                         "host"  : self.hostname
775                 }
776
777                 # Send update to the server.
778                 response = self.send_request(self.url, username=self.username, password=self.password,
779                         data=data)
780
781                 # Get the full response message.
782                 output = response.read()
783
784                 # Handle success messages.
785                 if output.startswith("<SUCCESS"):
786                         return
787
788                 # Handle error codes.
789                 if output.startswith("invalid login"):
790                         raise DDNSAuthenticationError
791                 elif output.startswith("<ERROR CODE=\"704\""):
792                         raise DDNSRequestError(_("No valid FQDN was given.")) 
793                 elif output.startswith("<ERROR CODE=\"702\""):
794                         raise DDNSInternalServerError
795
796                 # If we got here, some other update error happened.
797                 raise DDNSUpdateError