271f2c3658f88ba18148a0ded188878902f8b2c4
[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 subprocess
24 import urllib2
25 import xml.dom.minidom
26
27 from i18n import _
28
29 # Import all possible exception types.
30 from .errors import *
31
32 logger = logging.getLogger("ddns.providers")
33 logger.propagate = 1
34
35 _providers = {}
36
37 def get():
38         """
39                 Returns a dict with all automatically registered providers.
40         """
41         return _providers.copy()
42
43 class DDNSProvider(object):
44         # A short string that uniquely identifies
45         # this provider.
46         handle = None
47
48         # The full name of the provider.
49         name = None
50
51         # A weburl to the homepage of the provider.
52         # (Where to register a new account?)
53         website = None
54
55         # A list of supported protocols.
56         protocols = ("ipv6", "ipv4")
57
58         DEFAULT_SETTINGS = {}
59
60         # Automatically register all providers.
61         class __metaclass__(type):
62                 def __init__(provider, name, bases, dict):
63                         type.__init__(provider, name, bases, dict)
64
65                         # The main class from which is inherited is not registered
66                         # as a provider.
67                         if name == "DDNSProvider":
68                                 return
69
70                         if not all((provider.handle, provider.name, provider.website)):
71                                 raise DDNSError(_("Provider is not properly configured"))
72
73                         assert not _providers.has_key(provider.handle), \
74                                 "Provider '%s' has already been registered" % provider.handle
75
76                         _providers[provider.handle] = provider
77
78         def __init__(self, core, **settings):
79                 self.core = core
80
81                 # Copy a set of default settings and
82                 # update them by those from the configuration file.
83                 self.settings = self.DEFAULT_SETTINGS.copy()
84                 self.settings.update(settings)
85
86         def __repr__(self):
87                 return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
88
89         def __cmp__(self, other):
90                 return cmp(self.hostname, other.hostname)
91
92         def get(self, key, default=None):
93                 """
94                         Get a setting from the settings dictionary.
95                 """
96                 return self.settings.get(key, default)
97
98         @property
99         def hostname(self):
100                 """
101                         Fast access to the hostname.
102                 """
103                 return self.get("hostname")
104
105         @property
106         def username(self):
107                 """
108                         Fast access to the username.
109                 """
110                 return self.get("username")
111
112         @property
113         def password(self):
114                 """
115                         Fast access to the password.
116                 """
117                 return self.get("password")
118
119         @property
120         def token(self):
121                 """
122                         Fast access to the token.
123                 """
124                 return self.get("token")
125
126         def __call__(self, force=False):
127                 if force:
128                         logger.debug(_("Updating %s forced") % self.hostname)
129
130                 # Check if we actually need to update this host.
131                 elif self.is_uptodate(self.protocols):
132                         logger.debug(_("The dynamic host %(hostname)s (%(provider)s) is already up to date") % \
133                                 { "hostname" : self.hostname, "provider" : self.name })
134                         return
135
136                 # Execute the update.
137                 self.update()
138
139                 logger.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \
140                         { "hostname" : self.hostname, "provider" : self.name })
141
142         def update(self):
143                 raise NotImplementedError
144
145         def is_uptodate(self, protos):
146                 """
147                         Returns True if this host is already up to date
148                         and does not need to change the IP address on the
149                         name server.
150                 """
151                 for proto in protos:
152                         addresses = self.core.system.resolve(self.hostname, proto)
153
154                         current_address = self.get_address(proto)
155
156                         # If no addresses for the given protocol exist, we
157                         # are fine...
158                         if current_address is None and not addresses:
159                                 continue
160
161                         if not current_address in addresses:
162                                 return False
163
164                 return True
165
166         def send_request(self, *args, **kwargs):
167                 """
168                         Proxy connection to the send request
169                         method.
170                 """
171                 return self.core.system.send_request(*args, **kwargs)
172
173         def get_address(self, proto, default=None):
174                 """
175                         Proxy method to get the current IP address.
176                 """
177                 return self.core.system.get_address(proto) or default
178
179
180 class DDNSProtocolDynDNS2(object):
181         """
182                 This is an abstract class that implements the DynDNS updater
183                 protocol version 2. As this is a popular way to update dynamic
184                 DNS records, this class is supposed make the provider classes
185                 shorter and simpler.
186         """
187
188         # Information about the format of the request is to be found
189         # http://dyn.com/support/developers/api/perform-update/
190         # http://dyn.com/support/developers/api/return-codes/
191
192         def _prepare_request_data(self):
193                 data = {
194                         "hostname" : self.hostname,
195                         "myip"     : self.get_address("ipv4"),
196                 }
197
198                 return data
199
200         def update(self):
201                 data = self._prepare_request_data()
202
203                 # Send update to the server.
204                 response = self.send_request(self.url, data=data,
205                         username=self.username, password=self.password)
206
207                 # Get the full response message.
208                 output = response.read()
209
210                 # Handle success messages.
211                 if output.startswith("good") or output.startswith("nochg"):
212                         return
213
214                 # Handle error codes.
215                 if output == "badauth":
216                         raise DDNSAuthenticationError
217                 elif output == "abuse":
218                         raise DDNSAbuseError
219                 elif output == "notfqdn":
220                         raise DDNSRequestError(_("No valid FQDN was given."))
221                 elif output == "nohost":
222                         raise DDNSRequestError(_("Specified host does not exist."))
223                 elif output == "911":
224                         raise DDNSInternalServerError
225                 elif output == "dnserr":
226                         raise DDNSInternalServerError(_("DNS error encountered."))
227                 elif output == "badagent":
228                         raise DDNSBlockedError
229
230                 # If we got here, some other update error happened.
231                 raise DDNSUpdateError(_("Server response: %s") % output)
232
233
234 class DDNSResponseParserXML(object):
235         """
236                 This class provides a parser for XML responses which
237                 will be sent by various providers. This class uses the python
238                 shipped XML minidom module to walk through the XML tree and return
239                 a requested element.
240         """
241
242         def get_xml_tag_value(self, document, content):
243                 # Send input to the parser.
244                 xmldoc = xml.dom.minidom.parseString(document)
245
246                 # Get XML elements by the given content.
247                 element = xmldoc.getElementsByTagName(content)
248
249                 # If no element has been found, we directly can return None.
250                 if not element:
251                         return None
252
253                 # Only get the first child from an element, even there are more than one.
254                 firstchild = element[0].firstChild
255
256                 # Get the value of the child.
257                 value = firstchild.nodeValue
258
259                 # Return the value.
260                 return value
261
262
263 class DDNSProviderAllInkl(DDNSProvider):
264         handle    = "all-inkl.com"
265         name      = "All-inkl.com"
266         website   = "http://all-inkl.com/"
267         protocols = ("ipv4",)
268
269         # There are only information provided by the vendor how to
270         # perform an update on a FRITZ Box. Grab requried informations
271         # from the net.
272         # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
273
274         url = "http://dyndns.kasserver.com"
275
276         def update(self):
277                 # There is no additional data required so we directly can
278                 # send our request.
279                 response = self.send_request(self.url, username=self.username, password=self.password)
280
281                 # Get the full response message.
282                 output = response.read()
283
284                 # Handle success messages.
285                 if output.startswith("good") or output.startswith("nochg"):
286                         return
287
288                 # If we got here, some other update error happened.
289                 raise DDNSUpdateError
290
291
292 class DDNSProviderBindNsupdate(DDNSProvider):
293         handle  = "nsupdate"
294         name    = "BIND nsupdate utility"
295         website = "http://en.wikipedia.org/wiki/Nsupdate"
296
297         DEFAULT_TTL = 60
298
299         def update(self):
300                 scriptlet = self.__make_scriptlet()
301
302                 # -v enables TCP hence we transfer keys and other data that may
303                 # exceed the size of one packet.
304                 # -t sets the timeout
305                 command = ["nsupdate", "-v", "-t", "60"]
306
307                 p = subprocess.Popen(command, shell=True,
308                         stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
309                 )
310                 stdout, stderr = p.communicate(scriptlet)
311
312                 if p.returncode == 0:
313                         return
314
315                 raise DDNSError("nsupdate terminated with error code: %s\n  %s" % (p.returncode, stderr))
316
317         def __make_scriptlet(self):
318                 scriptlet = []
319
320                 # Set a different server the update is sent to.
321                 server = self.get("server", None)
322                 if server:
323                         scriptlet.append("server %s" % server)
324
325                 # Set the DNS zone the host should be added to.
326                 zone = self.get("zone", None)
327                 if zone:
328                         scriptlet.append("zone %s" % zone)
329
330                 key = self.get("key", None)
331                 if key:
332                         secret = self.get("secret")
333
334                         scriptlet.append("key %s %s" % (key, secret))
335
336                 ttl = self.get("ttl", self.DEFAULT_TTL)
337
338                 # Perform an update for each supported protocol.
339                 for rrtype, proto in (("AAAA", "ipv6"), ("A", "ipv4")):
340                         address = self.get_address(proto)
341                         if not address:
342                                 continue
343
344                         scriptlet.append("update delete %s. %s" % (self.hostname, rrtype))
345                         scriptlet.append("update add %s. %s %s %s" % \
346                                 (self.hostname, ttl, rrtype, address))
347
348                 # Send the actions to the server.
349                 scriptlet.append("send")
350                 scriptlet.append("quit")
351
352                 logger.debug(_("Scriptlet:"))
353                 for line in scriptlet:
354                         # Masquerade the line with the secret key.
355                         if line.startswith("key"):
356                                 line = "key **** ****"
357
358                         logger.debug("  %s" % line)
359
360                 return "\n".join(scriptlet)
361
362
363 class DDNSProviderDHS(DDNSProvider):
364         handle    = "dhs.org"
365         name      = "DHS International"
366         website   = "http://dhs.org/"
367         protocols = ("ipv4",)
368
369         # No information about the used update api provided on webpage,
370         # grabed from source code of ez-ipudate.
371
372         url = "http://members.dhs.org/nic/hosts"
373
374         def update(self):
375                 data = {
376                         "domain"       : self.hostname,
377                         "ip"           : self.get_address("ipv4"),
378                         "hostcmd"      : "edit",
379                         "hostcmdstage" : "2",
380                         "type"         : "4",
381                 }
382
383                 # Send update to the server.
384                 response = self.send_request(self.url, username=self.username, password=self.password,
385                         data=data)
386
387                 # Handle success messages.
388                 if response.code == 200:
389                         return
390
391                 # If we got here, some other update error happened.
392                 raise DDNSUpdateError
393
394
395 class DDNSProviderDNSpark(DDNSProvider):
396         handle    = "dnspark.com"
397         name      = "DNS Park"
398         website   = "http://dnspark.com/"
399         protocols = ("ipv4",)
400
401         # Informations to the used api can be found here:
402         # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
403
404         url = "https://control.dnspark.com/api/dynamic/update.php"
405
406         def update(self):
407                 data = {
408                         "domain" : self.hostname,
409                         "ip"     : self.get_address("ipv4"),
410                 }
411
412                 # Send update to the server.
413                 response = self.send_request(self.url, username=self.username, password=self.password,
414                         data=data)
415
416                 # Get the full response message.
417                 output = response.read()
418
419                 # Handle success messages.
420                 if output.startswith("ok") or output.startswith("nochange"):
421                         return
422
423                 # Handle error codes.
424                 if output == "unauth":
425                         raise DDNSAuthenticationError
426                 elif output == "abuse":
427                         raise DDNSAbuseError
428                 elif output == "blocked":
429                         raise DDNSBlockedError
430                 elif output == "nofqdn":
431                         raise DDNSRequestError(_("No valid FQDN was given."))
432                 elif output == "nohost":
433                         raise DDNSRequestError(_("Invalid hostname specified."))
434                 elif output == "notdyn":
435                         raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
436                 elif output == "invalid":
437                         raise DDNSRequestError(_("Invalid IP address has been sent."))
438
439                 # If we got here, some other update error happened.
440                 raise DDNSUpdateError
441
442
443 class DDNSProviderDtDNS(DDNSProvider):
444         handle    = "dtdns.com"
445         name      = "DtDNS"
446         website   = "http://dtdns.com/"
447         protocols = ("ipv4",)
448
449         # Information about the format of the HTTPS request is to be found
450         # http://www.dtdns.com/dtsite/updatespec
451
452         url = "https://www.dtdns.com/api/autodns.cfm"
453
454         def update(self):
455                 data = {
456                         "ip" : self.get_address("ipv4"),
457                         "id" : self.hostname,
458                         "pw" : self.password
459                 }
460
461                 # Send update to the server.
462                 response = self.send_request(self.url, data=data)
463
464                 # Get the full response message.
465                 output = response.read()
466
467                 # Remove all leading and trailing whitespace.
468                 output = output.strip()
469
470                 # Handle success messages.
471                 if "now points to" in output:
472                         return
473
474                 # Handle error codes.
475                 if output == "No hostname to update was supplied.":
476                         raise DDNSRequestError(_("No hostname specified."))
477
478                 elif output == "The hostname you supplied is not valid.":
479                         raise DDNSRequestError(_("Invalid hostname specified."))
480
481                 elif output == "The password you supplied is not valid.":
482                         raise DDNSAuthenticationError
483
484                 elif output == "Administration has disabled this account.":
485                         raise DDNSRequestError(_("Account has been disabled."))
486
487                 elif output == "Illegal character in IP.":
488                         raise DDNSRequestError(_("Invalid IP address has been sent."))
489
490                 elif output == "Too many failed requests.":
491                         raise DDNSRequestError(_("Too many failed requests."))
492
493                 # If we got here, some other update error happened.
494                 raise DDNSUpdateError
495
496
497 class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
498         handle    = "dyndns.org"
499         name      = "Dyn"
500         website   = "http://dyn.com/dns/"
501         protocols = ("ipv4",)
502
503         # Information about the format of the request is to be found
504         # http://http://dyn.com/support/developers/api/perform-update/
505         # http://dyn.com/support/developers/api/return-codes/
506
507         url = "https://members.dyndns.org/nic/update"
508
509
510 class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
511         handle    = "dynu.com"
512         name      = "Dynu"
513         website   = "http://dynu.com/"
514         protocols = ("ipv6", "ipv4",)
515
516         # Detailed information about the request and response codes
517         # are available on the providers webpage.
518         # http://dynu.com/Default.aspx?page=dnsapi
519
520         url = "https://api.dynu.com/nic/update"
521
522         def _prepare_request_data(self):
523                 data = DDNSProtocolDynDNS2._prepare_request_data(self)
524
525                 # This one supports IPv6
526                 myipv6 = self.get_address("ipv6")
527
528                 # Add update information if we have an IPv6 address.
529                 if myipv6:
530                         data["myipv6"] = myipv6
531
532                 return data
533
534
535 class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
536         handle    = "easydns.com"
537         name      = "EasyDNS"
538         website   = "http://www.easydns.com/"
539         protocols = ("ipv4",)
540
541         # There is only some basic documentation provided by the vendor,
542         # also searching the web gain very poor results.
543         # http://mediawiki.easydns.com/index.php/Dynamic_DNS
544
545         url = "http://api.cp.easydns.com/dyn/tomato.php"
546
547
548 class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
549         handle    = "domopoli.de"
550         name      = "domopoli.de"
551         website   = "http://domopoli.de/"
552         protocols = ("ipv4",)
553
554         # https://www.domopoli.de/?page=howto#DynDns_start
555
556         url = "http://dyndns.domopoli.de/nic/update"
557
558
559 class DDNSProviderDynsNet(DDNSProvider):
560         handle    = "dyns.net"
561         name      = "DyNS"
562         website   = "http://www.dyns.net/"
563         protocols = ("ipv4",)
564
565         # There is very detailed informatio about how to send the update request and
566         # the possible response codes. (Currently we are using the v1.1 proto)
567         # http://www.dyns.net/documentation/technical/protocol/
568
569         url = "http://www.dyns.net/postscript011.php"
570
571         def update(self):
572                 data = {
573                         "ip"       : self.get_address("ipv4"),
574                         "host"     : self.hostname,
575                         "username" : self.username,
576                         "password" : self.password,
577                 }
578
579                 # Send update to the server.
580                 response = self.send_request(self.url, data=data)
581
582                 # Get the full response message.
583                 output = response.read()
584
585                 # Handle success messages.
586                 if output.startswith("200"):
587                         return
588
589                 # Handle error codes.
590                 if output.startswith("400"):
591                         raise DDNSRequestError(_("Malformed request has been sent."))
592                 elif output.startswith("401"):
593                         raise DDNSAuthenticationError
594                 elif output.startswith("402"):
595                         raise DDNSRequestError(_("Too frequent update requests have been sent."))
596                 elif output.startswith("403"):
597                         raise DDNSInternalServerError
598
599                 # If we got here, some other update error happened.
600                 raise DDNSUpdateError(_("Server response: %s") % output) 
601
602
603 class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
604         handle    = "enom.com"
605         name      = "eNom Inc."
606         website   = "http://www.enom.com/"
607
608         # There are very detailed information about how to send an update request and
609         # the respone codes.
610         # http://www.enom.com/APICommandCatalog/
611
612         url = "https://dynamic.name-services.com/interface.asp"
613
614         def update(self):
615                 data = {
616                         "command"        : "setdnshost",
617                         "responsetype"   : "xml",
618                         "address"        : self.get_address("ipv4"),
619                         "domainpassword" : self.password,
620                         "zone"           : self.hostname
621                 }
622
623                 # Send update to the server.
624                 response = self.send_request(self.url, data=data)
625
626                 # Get the full response message.
627                 output = response.read()
628
629                 # Handle success messages.
630                 if self.get_xml_tag_value(output, "ErrCount") == "0":
631                         return
632
633                 # Handle error codes.
634                 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
635
636                 if errorcode == "304155":
637                         raise DDNSAuthenticationError
638                 elif errorcode == "304153":
639                         raise DDNSRequestError(_("Domain not found."))
640
641                 # If we got here, some other update error happened.
642                 raise DDNSUpdateError
643
644
645 class DDNSProviderEntryDNS(DDNSProvider):
646         handle    = "entrydns.net"
647         name      = "EntryDNS"
648         website   = "http://entrydns.net/"
649         protocols = ("ipv4",)
650
651         # Some very tiny details about their so called "Simple API" can be found
652         # here: https://entrydns.net/help
653         url = "https://entrydns.net/records/modify"
654
655         def update(self):
656                 data = {
657                         "ip" : self.get_address("ipv4")
658                 }
659
660                 # Add auth token to the update url.
661                 url = "%s/%s" % (self.url, self.token)
662
663                 # Send update to the server.
664                 try:
665                         response = self.send_request(url, data=data)
666
667                 # Handle error codes
668                 except urllib2.HTTPError, e:
669                         if e.code == 404:
670                                 raise DDNSAuthenticationError
671
672                         elif e.code == 422:
673                                 raise DDNSRequestError(_("An invalid IP address was submitted"))
674
675                         raise
676
677                 # Handle success messages.
678                 if response.code == 200:
679                         return
680
681                 # If we got here, some other update error happened.
682                 raise DDNSUpdateError
683
684
685 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
686         handle    = "freedns.afraid.org"
687         name      = "freedns.afraid.org"
688         website   = "http://freedns.afraid.org/"
689
690         # No information about the request or response could be found on the vendor
691         # page. All used values have been collected by testing.
692         url = "https://freedns.afraid.org/dynamic/update.php"
693
694         @property
695         def proto(self):
696                 return self.get("proto")
697
698         def update(self):
699                 address = self.get_address(self.proto)
700
701                 data = {
702                         "address" : address,
703                 }
704
705                 # Add auth token to the update url.
706                 url = "%s?%s" % (self.url, self.token)
707
708                 # Send update to the server.
709                 response = self.send_request(url, data=data)
710
711                 # Get the full response message.
712                 output = response.read()
713
714                 # Handle success messages.
715                 if output.startswith("Updated") or "has not changed" in output:
716                         return
717
718                 # Handle error codes.
719                 if output == "ERROR: Unable to locate this record":
720                         raise DDNSAuthenticationError
721                 elif "is an invalid IP address" in output:
722                         raise DDNSRequestError(_("Invalid IP address has been sent."))
723
724                 # If we got here, some other update error happened.
725                 raise DDNSUpdateError
726
727
728 class DDNSProviderLightningWireLabs(DDNSProvider):
729         handle    = "dns.lightningwirelabs.com"
730         name      = "Lightning Wire Labs DNS Service"
731         website   = "http://dns.lightningwirelabs.com/"
732
733         # Information about the format of the HTTPS request is to be found
734         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
735
736         url = "https://dns.lightningwirelabs.com/update"
737
738         def update(self):
739                 data =  {
740                         "hostname" : self.hostname,
741                         "address6" : self.get_address("ipv6", "-"),
742                         "address4" : self.get_address("ipv4", "-"),
743                 }
744
745                 # Check if a token has been set.
746                 if self.token:
747                         data["token"] = self.token
748
749                 # Check for username and password.
750                 elif self.username and self.password:
751                         data.update({
752                                 "username" : self.username,
753                                 "password" : self.password,
754                         })
755
756                 # Raise an error if no auth details are given.
757                 else:
758                         raise DDNSConfigurationError
759
760                 # Send update to the server.
761                 response = self.send_request(self.url, data=data)
762
763                 # Handle success messages.
764                 if response.code == 200:
765                         return
766
767                 # If we got here, some other update error happened.
768                 raise DDNSUpdateError
769
770
771 class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
772         handle    = "namecheap.com"
773         name      = "Namecheap"
774         website   = "http://namecheap.com"
775         protocols = ("ipv4",)
776
777         # Information about the format of the HTTP request is to be found
778         # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
779         # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
780
781         url = "https://dynamicdns.park-your-domain.com/update"
782
783         def update(self):
784                 # Namecheap requires the hostname splitted into a host and domain part.
785                 host, domain = self.hostname.split(".", 1)
786
787                 data = {
788                         "ip"       : self.get_address("ipv4"),
789                         "password" : self.password,
790                         "host"     : host,
791                         "domain"   : domain
792                 }
793
794                 # Send update to the server.
795                 response = self.send_request(self.url, data=data)
796
797                 # Get the full response message.
798                 output = response.read()
799
800                 # Handle success messages.
801                 if self.get_xml_tag_value(output, "IP") == self.get_address("ipv4"):
802                         return
803
804                 # Handle error codes.
805                 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
806
807                 if errorcode == "304156":
808                         raise DDNSAuthenticationError
809                 elif errorcode == "316153":
810                         raise DDNSRequestError(_("Domain not found."))
811                 elif errorcode == "316154":
812                         raise DDNSRequestError(_("Domain not active."))
813                 elif errorcode in ("380098", "380099"):
814                         raise DDNSInternalServerError
815
816                 # If we got here, some other update error happened.
817                 raise DDNSUpdateError
818
819
820 class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
821         handle    = "no-ip.com"
822         name      = "No-IP"
823         website   = "http://www.no-ip.com/"
824         protocols = ("ipv4",)
825
826         # Information about the format of the HTTP request is to be found
827         # here: http://www.no-ip.com/integrate/request and
828         # here: http://www.no-ip.com/integrate/response
829
830         url = "http://dynupdate.no-ip.com/nic/update"
831
832         def _prepare_request_data(self):
833                 data = {
834                         "hostname" : self.hostname,
835                         "address"  : self.get_address("ipv4"),
836                 }
837
838                 return data
839
840
841 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
842         handle    = "nsupdate.info"
843         name      = "nsupdate.info"
844         website   = "http://nsupdate.info/"
845         protocols = ("ipv6", "ipv4",)
846
847         # Information about the format of the HTTP request can be found
848         # after login on the provider user interface and here:
849         # http://nsupdateinfo.readthedocs.org/en/latest/user.html
850
851         # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
852         # and for the password a so called secret.
853         @property
854         def username(self):
855                 return self.get("hostname")
856
857         @property
858         def password(self):
859                 return self.get("secret")
860
861         @property
862         def proto(self):
863                 return self.get("proto")
864
865         @property
866         def url(self):
867                 # The update URL is different by the used protocol.
868                 if self.proto == "ipv4":
869                         return "https://ipv4.nsupdate.info/nic/update"
870                 elif self.proto == "ipv6":
871                         return "https://ipv6.nsupdate.info/nic/update"
872                 else:
873                         raise DDNSUpdateError(_("Invalid protocol has been given"))
874
875         def _prepare_request_data(self):
876                 data = {
877                         "myip" : self.get_address(self.proto),
878                 }
879
880                 return data
881
882
883 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
884         handle    = "opendns.com"
885         name      = "OpenDNS"
886         website   = "http://www.opendns.com"
887
888         # Detailed information about the update request and possible
889         # response codes can be obtained from here:
890         # https://support.opendns.com/entries/23891440
891
892         url = "https://updates.opendns.com/nic/update"
893
894         @property
895         def proto(self):
896                 return self.get("proto")
897
898         def _prepare_request_data(self):
899                 data = {
900                         "hostname" : self.hostname,
901                         "myip"     : self.get_address(self.proto)
902                 }
903
904                 return data
905
906
907 class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
908         handle    = "ovh.com"
909         name      = "OVH"
910         website   = "http://www.ovh.com/"
911         protocols = ("ipv4",)
912
913         # OVH only provides very limited information about how to
914         # update a DynDNS host. They only provide the update url
915         # on the their german subpage.
916         #
917         # http://hilfe.ovh.de/DomainDynHost
918
919         url = "https://www.ovh.com/nic/update"
920
921         def _prepare_request_data(self):
922                 data = DDNSProtocolDynDNS2._prepare_request_data(self)
923                 data.update({
924                         "system" : "dyndns",
925                 })
926
927                 return data
928
929
930 class DDNSProviderRegfish(DDNSProvider):
931         handle  = "regfish.com"
932         name    = "Regfish GmbH"
933         website = "http://www.regfish.com/"
934
935         # A full documentation to the providers api can be found here
936         # but is only available in german.
937         # https://www.regfish.de/domains/dyndns/dokumentation
938
939         url = "https://dyndns.regfish.de/"
940
941         def update(self):
942                 data = {
943                         "fqdn" : self.hostname,
944                 }
945
946                 # Check if we update an IPv6 address.
947                 address6 = self.get_address("ipv6")
948                 if address6:
949                         data["ipv6"] = address6
950
951                 # Check if we update an IPv4 address.
952                 address4 = self.get_address("ipv4")
953                 if address4:
954                         data["ipv4"] = address4
955
956                 # Raise an error if none address is given.
957                 if not data.has_key("ipv6") and not data.has_key("ipv4"):
958                         raise DDNSConfigurationError
959
960                 # Check if a token has been set.
961                 if self.token:
962                         data["token"] = self.token
963
964                 # Raise an error if no token and no useranem and password
965                 # are given.
966                 elif not self.username and not self.password:
967                         raise DDNSConfigurationError(_("No Auth details specified."))
968
969                 # HTTP Basic Auth is only allowed if no token is used.
970                 if self.token:
971                         # Send update to the server.
972                         response = self.send_request(self.url, data=data)
973                 else:
974                         # Send update to the server.
975                         response = self.send_request(self.url, username=self.username, password=self.password,
976                                 data=data)
977
978                 # Get the full response message.
979                 output = response.read()
980
981                 # Handle success messages.
982                 if "100" in output or "101" in output:
983                         return
984
985                 # Handle error codes.
986                 if "401" or "402" in output:
987                         raise DDNSAuthenticationError
988                 elif "408" in output:
989                         raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
990                 elif "409" in output:
991                         raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
992                 elif "412" in output:
993                         raise DDNSRequestError(_("No valid FQDN was given."))
994                 elif "414" in output:
995                         raise DDNSInternalServerError
996
997                 # If we got here, some other update error happened.
998                 raise DDNSUpdateError
999
1000
1001 class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
1002         handle    = "selfhost.de"
1003         name      = "Selfhost.de"
1004         website   = "http://www.selfhost.de/"
1005         protocols = ("ipv4",)
1006
1007         url = "https://carol.selfhost.de/nic/update"
1008
1009         def _prepare_request_data(self):
1010                 data = DDNSProtocolDynDNS2._prepare_request_data(self)
1011                 data.update({
1012                         "hostname" : "1",
1013                 })
1014
1015                 return data
1016
1017
1018 class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1019         handle    = "spdns.org"
1020         name      = "SPDNS"
1021         website   = "http://spdns.org/"
1022         protocols = ("ipv4",)
1023
1024         # Detailed information about request and response codes are provided
1025         # by the vendor. They are using almost the same mechanism and status
1026         # codes as dyndns.org so we can inherit all those stuff.
1027         #
1028         # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1029         # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1030
1031         url = "https://update.spdns.de/nic/update"
1032
1033         @property
1034         def username(self):
1035                 return self.get("username") or self.hostname
1036
1037         @property
1038         def password(self):
1039                 return self.get("username") or self.token
1040
1041
1042 class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1043         handle    = "strato.com"
1044         name      = "Strato AG"
1045         website   = "http:/www.strato.com/"
1046         protocols = ("ipv4",)
1047
1048         # Information about the request and response can be obtained here:
1049         # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1050
1051         url = "https://dyndns.strato.com/nic/update"
1052
1053
1054 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1055         handle    = "twodns.de"
1056         name      = "TwoDNS"
1057         website   = "http://www.twodns.de"
1058         protocols = ("ipv4",)
1059
1060         # Detailed information about the request can be found here
1061         # http://twodns.de/en/faqs
1062         # http://twodns.de/en/api
1063
1064         url = "https://update.twodns.de/update"
1065
1066         def _prepare_request_data(self):
1067                 data = {
1068                         "ip" : self.get_address("ipv4"),
1069                         "hostname" : self.hostname
1070                 }
1071
1072                 return data
1073
1074
1075 class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1076         handle    = "udmedia.de"
1077         name      = "Udmedia GmbH"
1078         website   = "http://www.udmedia.de"
1079         protocols = ("ipv4",)
1080
1081         # Information about the request can be found here
1082         # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1083
1084         url = "https://www.udmedia.de/nic/update"
1085
1086
1087 class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
1088         handle    = "variomedia.de"
1089         name      = "Variomedia"
1090         website   = "http://www.variomedia.de/"
1091         protocols = ("ipv6", "ipv4",)
1092
1093         # Detailed information about the request can be found here
1094         # https://dyndns.variomedia.de/
1095
1096         url = "https://dyndns.variomedia.de/nic/update"
1097
1098         @property
1099         def proto(self):
1100                 return self.get("proto")
1101
1102         def _prepare_request_data(self):
1103                 data = {
1104                         "hostname" : self.hostname,
1105                         "myip"     : self.get_address(self.proto)
1106                 }
1107
1108                 return data
1109
1110
1111 class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
1112         handle    = "zoneedit.com"
1113         name      = "Zoneedit"
1114         website   = "http://www.zoneedit.com"
1115         protocols = ("ipv4",)
1116
1117         # Detailed information about the request and the response codes can be
1118         # obtained here:
1119         # http://www.zoneedit.com/doc/api/other.html
1120         # http://www.zoneedit.com/faq.html
1121
1122         url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1123
1124         @property
1125         def proto(self):
1126                 return self.get("proto")
1127
1128         def update(self):
1129                 data = {
1130                         "dnsto" : self.get_address(self.proto),
1131                         "host"  : self.hostname
1132                 }
1133
1134                 # Send update to the server.
1135                 response = self.send_request(self.url, username=self.username, password=self.password,
1136                         data=data)
1137
1138                 # Get the full response message.
1139                 output = response.read()
1140
1141                 # Handle success messages.
1142                 if output.startswith("<SUCCESS"):
1143                         return
1144
1145                 # Handle error codes.
1146                 if output.startswith("invalid login"):
1147                         raise DDNSAuthenticationError
1148                 elif output.startswith("<ERROR CODE=\"704\""):
1149                         raise DDNSRequestError(_("No valid FQDN was given.")) 
1150                 elif output.startswith("<ERROR CODE=\"702\""):
1151                         raise DDNSInternalServerError
1152
1153                 # If we got here, some other update error happened.
1154                 raise DDNSUpdateError
1155
1156
1157 class DDNSProviderZZZZ(DDNSProvider):
1158         handle    = "zzzz.io"
1159         name      = "zzzz"
1160         website   = "https://zzzz.io"
1161         protocols = ("ipv4",)
1162
1163         # Detailed information about the update request can be found here:
1164         # https://zzzz.io/faq/
1165
1166         # Details about the possible response codes have been provided in the bugtracker:
1167         # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1168
1169         url = "https://zzzz.io/api/v1/update"
1170
1171         def update(self):
1172                 data = {
1173                         "ip"    : self.get_address("ipv4"),
1174                         "token" : self.token,
1175                 }
1176
1177                 # zzzz uses the host from the full hostname as part
1178                 # of the update url.
1179                 host, domain = self.hostname.split(".", 1)
1180
1181                 # Add host value to the update url.
1182                 url = "%s/%s" % (self.url, host)
1183
1184                 # Send update to the server.
1185                 try:
1186                         response = self.send_request(url, data=data)
1187
1188                 # Handle error codes.
1189                 except DDNSNotFound:
1190                         raise DDNSRequestError(_("Invalid hostname specified"))
1191
1192                 # Handle success messages.
1193                 if response.code == 200:
1194                         return
1195
1196                 # If we got here, some other update error happened.
1197                 raise DDNSUpdateError