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