]> git.ipfire.org Git - ddns.git/blobdiff - src/ddns/providers.py
DynU: Fix typo in function call
[ddns.git] / src / ddns / providers.py
index 383d7995b8e0fd637597ca632248b8cfec9c6260..0bd21b83939c11d75af83c35f993537275b43e0b 100644 (file)
 #                                                                             #
 ###############################################################################
 
 #                                                                             #
 ###############################################################################
 
+import datetime
 import logging
 import logging
+import os
+import subprocess
 import urllib2
 import xml.dom.minidom
 
 import urllib2
 import xml.dom.minidom
 
@@ -31,6 +34,14 @@ from .errors import *
 logger = logging.getLogger("ddns.providers")
 logger.propagate = 1
 
 logger = logging.getLogger("ddns.providers")
 logger.propagate = 1
 
+_providers = {}
+
+def get():
+       """
+               Returns a dict with all automatically registered providers.
+       """
+       return _providers.copy()
+
 class DDNSProvider(object):
        # A short string that uniquely identifies
        # this provider.
 class DDNSProvider(object):
        # A short string that uniquely identifies
        # this provider.
@@ -48,6 +59,40 @@ class DDNSProvider(object):
 
        DEFAULT_SETTINGS = {}
 
 
        DEFAULT_SETTINGS = {}
 
+       # holdoff time - Number of days no update is performed unless
+       # the IP address has changed.
+       holdoff_days = 30
+
+       # True if the provider is able to remove records, too.
+       # Required to remove AAAA records if IPv6 is absent again.
+       can_remove_records = True
+
+       # Automatically register all providers.
+       class __metaclass__(type):
+               def __init__(provider, name, bases, dict):
+                       type.__init__(provider, name, bases, dict)
+
+                       # The main class from which is inherited is not registered
+                       # as a provider.
+                       if name == "DDNSProvider":
+                               return
+
+                       if not all((provider.handle, provider.name, provider.website)):
+                               raise DDNSError(_("Provider is not properly configured"))
+
+                       assert not _providers.has_key(provider.handle), \
+                               "Provider '%s' has already been registered" % provider.handle
+
+                       _providers[provider.handle] = provider
+
+       @staticmethod
+       def supported():
+               """
+                       Should be overwritten to check if the system the code is running
+                       on has all the required tools to support this provider.
+               """
+               return True
+
        def __init__(self, core, **settings):
                self.core = core
 
        def __init__(self, core, **settings):
                self.core = core
 
@@ -62,6 +107,10 @@ class DDNSProvider(object):
        def __cmp__(self, other):
                return cmp(self.hostname, other.hostname)
 
        def __cmp__(self, other):
                return cmp(self.hostname, other.hostname)
 
+       @property
+       def db(self):
+               return self.core.db
+
        def get(self, key, default=None):
                """
                        Get a setting from the settings dictionary.
        def get(self, key, default=None):
                """
                        Get a setting from the settings dictionary.
@@ -100,18 +149,65 @@ class DDNSProvider(object):
                if force:
                        logger.debug(_("Updating %s forced") % self.hostname)
 
                if force:
                        logger.debug(_("Updating %s forced") % self.hostname)
 
-               # Check if we actually need to update this host.
-               elif self.is_uptodate(self.protocols):
-                       logger.debug(_("%s is already up to date") % self.hostname)
+               # Do nothing if no update is required
+               elif not self.requires_update:
                        return
 
                # Execute the update.
                        return
 
                # Execute the update.
-               self.update()
+               try:
+                       self.update()
+
+               # In case of any errors, log the failed request and
+               # raise the exception.
+               except DDNSError as e:
+                       self.core.db.log_failure(self.hostname, e)
+                       raise
+
+               logger.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \
+                       { "hostname" : self.hostname, "provider" : self.name })
+               self.core.db.log_success(self.hostname)
 
        def update(self):
 
        def update(self):
+               for protocol in self.protocols:
+                       if self.have_address(protocol):
+                               self.update_protocol(protocol)
+                       elif self.can_remove_records:
+                               self.remove_protocol(protocol)
+
+       def update_protocol(self, proto):
                raise NotImplementedError
 
                raise NotImplementedError
 
-       def is_uptodate(self, protos):
+       def remove_protocol(self, proto):
+               if not self.can_remove_records:
+                       raise RuntimeError, "can_remove_records is enabled, but remove_protocol() not implemented"
+
+               raise NotImplementedError
+
+       @property
+       def requires_update(self):
+               # If the IP addresses have changed, an update is required
+               if self.ip_address_changed(self.protocols):
+                       logger.debug(_("An update for %(hostname)s (%(provider)s)"
+                               " is performed because of an IP address change") % \
+                               { "hostname" : self.hostname, "provider" : self.name })
+
+                       return True
+
+               # If the holdoff time has expired, an update is required, too
+               if self.holdoff_time_expired():
+                       logger.debug(_("An update for %(hostname)s (%(provider)s)"
+                               " is performed because the holdoff time has expired") % \
+                               { "hostname" : self.hostname, "provider" : self.name })
+
+                       return True
+
+               # Otherwise, we don't need to perform an update
+               logger.debug(_("No update required for %(hostname)s (%(provider)s)") % \
+                       { "hostname" : self.hostname, "provider" : self.name })
+
+               return False
+
+       def ip_address_changed(self, protos):
                """
                        Returns True if this host is already up to date
                        and does not need to change the IP address on the
                """
                        Returns True if this host is already up to date
                        and does not need to change the IP address on the
@@ -119,18 +215,53 @@ class DDNSProvider(object):
                """
                for proto in protos:
                        addresses = self.core.system.resolve(self.hostname, proto)
                """
                for proto in protos:
                        addresses = self.core.system.resolve(self.hostname, proto)
-
                        current_address = self.get_address(proto)
 
                        current_address = self.get_address(proto)
 
-                       # If no addresses for the given protocol exist, we
-                       # are fine...
-                       if current_address is None and not addresses:
+                       # Handle if the system has not got any IP address from a protocol
+                       # (i.e. had full dual-stack connectivity which it has not any more)
+                       if current_address is None:
+                               # If addresses still exists in the DNS system and if this provider
+                               # is able to remove records, we will do that.
+                               if addresses and self.can_remove_records:
+                                       return True
+
+                               # Otherwise, we cannot go on...
                                continue
 
                        if not current_address in addresses:
                                continue
 
                        if not current_address in addresses:
-                               return False
+                               return True
 
 
-               return True
+               return False
+
+       def holdoff_time_expired(self):
+               """
+                       Returns true if the holdoff time has expired
+                       and the host requires an update
+               """
+               # If no holdoff days is defined, we cannot go on
+               if not self.holdoff_days:
+                       return False
+
+               # Get the timestamp of the last successfull update
+               last_update = self.db.last_update(self.hostname)
+
+               # If no timestamp has been recorded, no update has been
+               # performed. An update should be performed now.
+               if not last_update:
+                       return True
+
+               # Determine when the holdoff time ends
+               holdoff_end = last_update + datetime.timedelta(days=self.holdoff_days)
+
+               now = datetime.datetime.utcnow()
+
+               if now >= holdoff_end:
+                       logger.debug("The holdoff time has expired for %s" % self.hostname)
+                       return True
+               else:
+                       logger.debug("Updates for %s are held off until %s" % \
+                               (self.hostname, holdoff_end))
+                       return False
 
        def send_request(self, *args, **kwargs):
                """
 
        def send_request(self, *args, **kwargs):
                """
@@ -145,6 +276,107 @@ class DDNSProvider(object):
                """
                return self.core.system.get_address(proto) or default
 
                """
                return self.core.system.get_address(proto) or default
 
+       def have_address(self, proto):
+               """
+                       Returns True if an IP address for the given protocol
+                       is known and usable.
+               """
+               address = self.get_address(proto)
+
+               if address:
+                       return True
+
+               return False
+
+
+class DDNSProtocolDynDNS2(object):
+       """
+               This is an abstract class that implements the DynDNS updater
+               protocol version 2. As this is a popular way to update dynamic
+               DNS records, this class is supposed make the provider classes
+               shorter and simpler.
+       """
+
+       # Information about the format of the request is to be found
+       # http://dyn.com/support/developers/api/perform-update/
+       # http://dyn.com/support/developers/api/return-codes/
+
+       # The DynDNS protocol version 2 does not allow to remove records
+       can_remove_records = False
+
+       def prepare_request_data(self, proto):
+               data = {
+                       "hostname" : self.hostname,
+                       "myip"     : self.get_address(proto),
+               }
+
+               return data
+
+       def update_protocol(self, proto):
+               data = self.prepare_request_data(proto)
+
+               return self.send_request(data)
+
+       def send_request(self, data):
+               # Send update to the server.
+               response = DDNSProvider.send_request(self, self.url, data=data,
+                       username=self.username, password=self.password)
+
+               # Get the full response message.
+               output = response.read()
+
+               # Handle success messages.
+               if output.startswith("good") or output.startswith("nochg"):
+                       return
+
+               # Handle error codes.
+               if output == "badauth":
+                       raise DDNSAuthenticationError
+               elif output == "abuse":
+                       raise DDNSAbuseError
+               elif output == "notfqdn":
+                       raise DDNSRequestError(_("No valid FQDN was given."))
+               elif output == "nohost":
+                       raise DDNSRequestError(_("Specified host does not exist."))
+               elif output == "911":
+                       raise DDNSInternalServerError
+               elif output == "dnserr":
+                       raise DDNSInternalServerError(_("DNS error encountered."))
+               elif output == "badagent":
+                       raise DDNSBlockedError
+
+               # If we got here, some other update error happened.
+               raise DDNSUpdateError(_("Server response: %s") % output)
+
+
+class DDNSResponseParserXML(object):
+       """
+               This class provides a parser for XML responses which
+               will be sent by various providers. This class uses the python
+               shipped XML minidom module to walk through the XML tree and return
+               a requested element.
+        """
+
+       def get_xml_tag_value(self, document, content):
+               # Send input to the parser.
+               xmldoc = xml.dom.minidom.parseString(document)
+
+               # Get XML elements by the given content.
+               element = xmldoc.getElementsByTagName(content)
+
+               # If no element has been found, we directly can return None.
+               if not element:
+                       return None
+
+               # Only get the first child from an element, even there are more than one.
+               firstchild = element[0].firstChild
+
+               # Get the value of the child.
+               value = firstchild.nodeValue
+
+               # Return the value.
+               return value
+
 
 class DDNSProviderAllInkl(DDNSProvider):
        handle    = "all-inkl.com"
 
 class DDNSProviderAllInkl(DDNSProvider):
        handle    = "all-inkl.com"
@@ -158,6 +390,7 @@ class DDNSProviderAllInkl(DDNSProvider):
        # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
 
        url = "http://dyndns.kasserver.com"
        # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
 
        url = "http://dyndns.kasserver.com"
+       can_remove_records = False
 
        def update(self):
                # There is no additional data required so we directly can
 
        def update(self):
                # There is no additional data required so we directly can
@@ -175,6 +408,90 @@ class DDNSProviderAllInkl(DDNSProvider):
                raise DDNSUpdateError
 
 
                raise DDNSUpdateError
 
 
+class DDNSProviderBindNsupdate(DDNSProvider):
+       handle  = "nsupdate"
+       name    = "BIND nsupdate utility"
+       website = "http://en.wikipedia.org/wiki/Nsupdate"
+
+       DEFAULT_TTL = 60
+
+       @staticmethod
+       def supported():
+               # Search if the nsupdate utility is available
+               paths = os.environ.get("PATH")
+
+               for path in paths.split(":"):
+                       executable = os.path.join(path, "nsupdate")
+
+                       if os.path.exists(executable):
+                               return True
+
+               return False
+
+       def update(self):
+               scriptlet = self.__make_scriptlet()
+
+               # -v enables TCP hence we transfer keys and other data that may
+               # exceed the size of one packet.
+               # -t sets the timeout
+               command = ["nsupdate", "-v", "-t", "60"]
+
+               p = subprocess.Popen(command, shell=True,
+                       stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+               )
+               stdout, stderr = p.communicate(scriptlet)
+
+               if p.returncode == 0:
+                       return
+
+               raise DDNSError("nsupdate terminated with error code: %s\n  %s" % (p.returncode, stderr))
+
+       def __make_scriptlet(self):
+               scriptlet = []
+
+               # Set a different server the update is sent to.
+               server = self.get("server", None)
+               if server:
+                       scriptlet.append("server %s" % server)
+
+               # Set the DNS zone the host should be added to.
+               zone = self.get("zone", None)
+               if zone:
+                       scriptlet.append("zone %s" % zone)
+
+               key = self.get("key", None)
+               if key:
+                       secret = self.get("secret")
+
+                       scriptlet.append("key %s %s" % (key, secret))
+
+               ttl = self.get("ttl", self.DEFAULT_TTL)
+
+               # Perform an update for each supported protocol.
+               for rrtype, proto in (("AAAA", "ipv6"), ("A", "ipv4")):
+                       address = self.get_address(proto)
+                       if not address:
+                               continue
+
+                       scriptlet.append("update delete %s. %s" % (self.hostname, rrtype))
+                       scriptlet.append("update add %s. %s %s %s" % \
+                               (self.hostname, ttl, rrtype, address))
+
+               # Send the actions to the server.
+               scriptlet.append("send")
+               scriptlet.append("quit")
+
+               logger.debug(_("Scriptlet:"))
+               for line in scriptlet:
+                       # Masquerade the line with the secret key.
+                       if line.startswith("key"):
+                               line = "key **** ****"
+
+                       logger.debug("  %s" % line)
+
+               return "\n".join(scriptlet)
+
+
 class DDNSProviderDHS(DDNSProvider):
        handle    = "dhs.org"
        name      = "DHS International"
 class DDNSProviderDHS(DDNSProvider):
        handle    = "dhs.org"
        name      = "DHS International"
@@ -183,12 +500,14 @@ class DDNSProviderDHS(DDNSProvider):
 
        # No information about the used update api provided on webpage,
        # grabed from source code of ez-ipudate.
 
        # No information about the used update api provided on webpage,
        # grabed from source code of ez-ipudate.
+
        url = "http://members.dhs.org/nic/hosts"
        url = "http://members.dhs.org/nic/hosts"
+       can_remove_records = False
 
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                        "domain"       : self.hostname,
                data = {
                        "domain"       : self.hostname,
-                       "ip"           : self.get_address("ipv4"),
+                       "ip"           : self.get_address(proto),
                        "hostcmd"      : "edit",
                        "hostcmdstage" : "2",
                        "type"         : "4",
                        "hostcmd"      : "edit",
                        "hostcmdstage" : "2",
                        "type"         : "4",
@@ -214,12 +533,14 @@ class DDNSProviderDNSpark(DDNSProvider):
 
        # Informations to the used api can be found here:
        # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
 
        # Informations to the used api can be found here:
        # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
+
        url = "https://control.dnspark.com/api/dynamic/update.php"
        url = "https://control.dnspark.com/api/dynamic/update.php"
+       can_remove_records = False
 
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                        "domain" : self.hostname,
                data = {
                        "domain" : self.hostname,
-                       "ip"     : self.get_address("ipv4"),
+                       "ip"     : self.get_address(proto),
                }
 
                # Send update to the server.
                }
 
                # Send update to the server.
@@ -261,11 +582,13 @@ class DDNSProviderDtDNS(DDNSProvider):
 
        # Information about the format of the HTTPS request is to be found
        # http://www.dtdns.com/dtsite/updatespec
 
        # Information about the format of the HTTPS request is to be found
        # http://www.dtdns.com/dtsite/updatespec
+
        url = "https://www.dtdns.com/api/autodns.cfm"
        url = "https://www.dtdns.com/api/autodns.cfm"
+       can_remove_records = False
 
 
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                data = {
-                       "ip" : self.get_address("ipv4"),
+                       "ip" : self.get_address(proto),
                        "id" : self.hostname,
                        "pw" : self.password
                }
                        "id" : self.hostname,
                        "pw" : self.password
                }
@@ -306,7 +629,7 @@ class DDNSProviderDtDNS(DDNSProvider):
                raise DDNSUpdateError
 
 
                raise DDNSUpdateError
 
 
-class DDNSProviderDynDNS(DDNSProvider):
+class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
        handle    = "dyndns.org"
        name      = "Dyn"
        website   = "http://dyn.com/dns/"
        handle    = "dyndns.org"
        name      = "Dyn"
        website   = "http://dyn.com/dns/"
@@ -315,81 +638,189 @@ class DDNSProviderDynDNS(DDNSProvider):
        # Information about the format of the request is to be found
        # http://http://dyn.com/support/developers/api/perform-update/
        # http://dyn.com/support/developers/api/return-codes/
        # Information about the format of the request is to be found
        # http://http://dyn.com/support/developers/api/perform-update/
        # http://dyn.com/support/developers/api/return-codes/
+
        url = "https://members.dyndns.org/nic/update"
 
        url = "https://members.dyndns.org/nic/update"
 
-       def _prepare_request_data(self):
-               data = {
-                       "hostname" : self.hostname,
-                       "myip"     : self.get_address("ipv4"),
-               }
 
 
-               return data
+class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "dynu.com"
+       name      = "Dynu"
+       website   = "http://dynu.com/"
+       protocols = ("ipv6", "ipv4",)
+
+       # Detailed information about the request and response codes
+       # are available on the providers webpage.
+       # http://dynu.com/Default.aspx?page=dnsapi
+
+       url = "https://api.dynu.com/nic/update"
+
+       # DynU sends the IPv6 and IPv4 address in one request
 
        def update(self):
 
        def update(self):
-               data = self._prepare_request_data()
+               data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
+
+               # This one supports IPv6
+               myipv6 = self.get_address("ipv6")
+
+               # Add update information if we have an IPv6 address.
+               if myipv6:
+                       data["myipv6"] = myipv6
+
+               self.send_request(data)
+
+
+class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "easydns.com"
+       name      = "EasyDNS"
+       website   = "http://www.easydns.com/"
+       protocols = ("ipv4",)
+
+       # There is only some basic documentation provided by the vendor,
+       # also searching the web gain very poor results.
+       # http://mediawiki.easydns.com/index.php/Dynamic_DNS
+
+       url = "http://api.cp.easydns.com/dyn/tomato.php"
+
+
+class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "domopoli.de"
+       name      = "domopoli.de"
+       website   = "http://domopoli.de/"
+       protocols = ("ipv4",)
+
+       # https://www.domopoli.de/?page=howto#DynDns_start
+
+       url = "http://dyndns.domopoli.de/nic/update"
+
+
+class DDNSProviderDynsNet(DDNSProvider):
+       handle    = "dyns.net"
+       name      = "DyNS"
+       website   = "http://www.dyns.net/"
+       protocols = ("ipv4",)
+       can_remove_records = False
+
+       # There is very detailed informatio about how to send the update request and
+       # the possible response codes. (Currently we are using the v1.1 proto)
+       # http://www.dyns.net/documentation/technical/protocol/
+
+       url = "http://www.dyns.net/postscript011.php"
+
+       def update_protocol(self, proto):
+               data = {
+                       "ip"       : self.get_address(proto),
+                       "host"     : self.hostname,
+                       "username" : self.username,
+                       "password" : self.password,
+               }
 
                # Send update to the server.
 
                # Send update to the server.
-               response = self.send_request(self.url, data=data,
-                       username=self.username, password=self.password)
+               response = self.send_request(self.url, data=data)
 
                # Get the full response message.
                output = response.read()
 
                # Handle success messages.
 
                # Get the full response message.
                output = response.read()
 
                # Handle success messages.
-               if output.startswith("good") or output.startswith("nochg"):
+               if output.startswith("200"):
                        return
 
                # Handle error codes.
                        return
 
                # Handle error codes.
-               if output == "badauth":
+               if output.startswith("400"):
+                       raise DDNSRequestError(_("Malformed request has been sent."))
+               elif output.startswith("401"):
                        raise DDNSAuthenticationError
                        raise DDNSAuthenticationError
-               elif output == "aduse":
-                       raise DDNSAbuseError
-               elif output == "notfqdn":
-                       raise DDNSRequestError(_("No valid FQDN was given."))
-               elif output == "nohost":
-                       raise DDNSRequestError(_("Specified host does not exist."))
-               elif output == "911":
+               elif output.startswith("402"):
+                       raise DDNSRequestError(_("Too frequent update requests have been sent."))
+               elif output.startswith("403"):
                        raise DDNSInternalServerError
                        raise DDNSInternalServerError
-               elif output == "dnserr":
-                       raise DDNSInternalServerError(_("DNS error encountered."))
 
                # If we got here, some other update error happened.
 
                # If we got here, some other update error happened.
-               raise DDNSUpdateError(_("Server response: %s") % output)
+               raise DDNSUpdateError(_("Server response: %s") % output) 
 
 
 
 
-class DDNSProviderDynU(DDNSProviderDynDNS):
-       handle    = "dynu.com"
-       name      = "Dynu"
-       website   = "http://dynu.com/"
-       protocols = ("ipv6", "ipv4",)
+class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
+       handle    = "enom.com"
+       name      = "eNom Inc."
+       website   = "http://www.enom.com/"
+       protocols = ("ipv4",)
 
 
-       # Detailed information about the request and response codes
-       # are available on the providers webpage.
-       # http://dynu.com/Default.aspx?page=dnsapi
+       # There are very detailed information about how to send an update request and
+       # the respone codes.
+       # http://www.enom.com/APICommandCatalog/
 
 
-       url = "https://api.dynu.com/nic/update"
+       url = "https://dynamic.name-services.com/interface.asp"
+       can_remove_records = False
 
 
-       def _prepare_request_data(self):
-               data = DDNSProviderDynDNS._prepare_request_data(self)
+       def update_protocol(self, proto):
+               data = {
+                       "command"        : "setdnshost",
+                       "responsetype"   : "xml",
+                       "address"        : self.get_address(proto),
+                       "domainpassword" : self.password,
+                       "zone"           : self.hostname
+               }
 
 
-               # This one supports IPv6
-               data.update({
-                       "myipv6"   : self.get_address("ipv6"),
-               })
+               # Send update to the server.
+               response = self.send_request(self.url, data=data)
 
 
-               return data
+               # Get the full response message.
+               output = response.read()
 
 
+               # Handle success messages.
+               if self.get_xml_tag_value(output, "ErrCount") == "0":
+                       return
 
 
-class DDNSProviderEasyDNS(DDNSProviderDynDNS):
-       handle  = "easydns.com"
-       name    = "EasyDNS"
-       website = "http://www.easydns.com/"
+               # Handle error codes.
+               errorcode = self.get_xml_tag_value(output, "ResponseNumber")
 
 
-       # There is only some basic documentation provided by the vendor,
-       # also searching the web gain very poor results.
-       # http://mediawiki.easydns.com/index.php/Dynamic_DNS
+               if errorcode == "304155":
+                       raise DDNSAuthenticationError
+               elif errorcode == "304153":
+                       raise DDNSRequestError(_("Domain not found."))
 
 
-       url = "http://api.cp.easydns.com/dyn/tomato.php"
+               # If we got here, some other update error happened.
+               raise DDNSUpdateError
+
+
+class DDNSProviderEntryDNS(DDNSProvider):
+       handle    = "entrydns.net"
+       name      = "EntryDNS"
+       website   = "http://entrydns.net/"
+       protocols = ("ipv4",)
+
+       # Some very tiny details about their so called "Simple API" can be found
+       # here: https://entrydns.net/help
+       url = "https://entrydns.net/records/modify"
+       can_remove_records = False
+
+       def update_protocol(self, proto):
+               data = {
+                       "ip" : self.get_address(proto),
+               }
+
+               # Add auth token to the update url.
+               url = "%s/%s" % (self.url, self.token)
+
+               # Send update to the server.
+               try:
+                       response = self.send_request(url, data=data)
+
+               # Handle error codes
+               except urllib2.HTTPError, e:
+                       if e.code == 404:
+                               raise DDNSAuthenticationError
+
+                       elif e.code == 422:
+                               raise DDNSRequestError(_("An invalid IP address was submitted"))
+
+                       raise
+
+               # Handle success messages.
+               if response.code == 200:
+                       return
+
+               # If we got here, some other update error happened.
+               raise DDNSUpdateError
 
 
 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
 
 
 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
@@ -400,16 +831,11 @@ class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
        # No information about the request or response could be found on the vendor
        # page. All used values have been collected by testing.
        url = "https://freedns.afraid.org/dynamic/update.php"
        # No information about the request or response could be found on the vendor
        # page. All used values have been collected by testing.
        url = "https://freedns.afraid.org/dynamic/update.php"
+       can_remove_records = False
 
 
-       @property
-       def proto(self):
-               return self.get("proto")
-
-       def update(self):
-               address = self.get_address(self.proto)
-
+       def update_protocol(self, proto):
                data = {
                data = {
-                       "address" : address,
+                       "address" : self.get_address(proto),
                }
 
                # Add auth token to the update url.
                }
 
                # Add auth token to the update url.
@@ -418,6 +844,10 @@ class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
                # Send update to the server.
                response = self.send_request(url, data=data)
 
                # Send update to the server.
                response = self.send_request(url, data=data)
 
+               # Get the full response message.
+               output = response.read()
+
+               # Handle success messages.
                if output.startswith("Updated") or "has not changed" in output:
                        return
 
                if output.startswith("Updated") or "has not changed" in output:
                        return
 
@@ -427,6 +857,9 @@ class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
                elif "is an invalid IP address" in output:
                        raise DDNSRequestError(_("Invalid IP address has been sent."))
 
                elif "is an invalid IP address" in output:
                        raise DDNSRequestError(_("Invalid IP address has been sent."))
 
+               # If we got here, some other update error happened.
+               raise DDNSUpdateError
+
 
 class DDNSProviderLightningWireLabs(DDNSProvider):
        handle    = "dns.lightningwirelabs.com"
 
 class DDNSProviderLightningWireLabs(DDNSProvider):
        handle    = "dns.lightningwirelabs.com"
@@ -435,6 +868,7 @@ class DDNSProviderLightningWireLabs(DDNSProvider):
 
        # Information about the format of the HTTPS request is to be found
        # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
 
        # Information about the format of the HTTPS request is to be found
        # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
+
        url = "https://dns.lightningwirelabs.com/update"
 
        def update(self):
        url = "https://dns.lightningwirelabs.com/update"
 
        def update(self):
@@ -470,7 +904,26 @@ class DDNSProviderLightningWireLabs(DDNSProvider):
                raise DDNSUpdateError
 
 
                raise DDNSUpdateError
 
 
-class DDNSProviderNamecheap(DDNSProvider):
+class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "myonlineportal.net"
+       name      = "myonlineportal.net"
+       website   = "https:/myonlineportal.net/"
+
+       # Information about the request and response can be obtained here:
+       # https://myonlineportal.net/howto_dyndns
+
+       url = "https://myonlineportal.net/updateddns"
+
+       def prepare_request_data(self, proto):
+               data = {
+                       "hostname" : self.hostname,
+                       "ip"     : self.get_address(proto),
+               }
+
+               return data
+
+
+class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
        handle    = "namecheap.com"
        name      = "Namecheap"
        website   = "http://namecheap.com"
        handle    = "namecheap.com"
        name      = "Namecheap"
        website   = "http://namecheap.com"
@@ -481,33 +934,14 @@ class DDNSProviderNamecheap(DDNSProvider):
        # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
 
        url = "https://dynamicdns.park-your-domain.com/update"
        # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
 
        url = "https://dynamicdns.park-your-domain.com/update"
+       can_remove_records = False
 
 
-       def parse_xml(self, document, content):
-               # Send input to the parser.
-               xmldoc = xml.dom.minidom.parseString(document)
-
-               # Get XML elements by the given content.
-               element = xmldoc.getElementsByTagName(content)
-
-               # If no element has been found, we directly can return None.
-               if not element:
-                       return None
-
-               # Only get the first child from an element, even there are more than one.
-               firstchild = element[0].firstChild
-
-               # Get the value of the child.
-               value = firstchild.nodeValue
-
-               # Return the value.
-               return value
-               
-       def update(self):
+       def update_protocol(self, proto):
                # Namecheap requires the hostname splitted into a host and domain part.
                host, domain = self.hostname.split(".", 1)
 
                data = {
                # Namecheap requires the hostname splitted into a host and domain part.
                host, domain = self.hostname.split(".", 1)
 
                data = {
-                       "ip"       : self.get_address("ipv4"),
+                       "ip"       : self.get_address(proto),
                        "password" : self.password,
                        "host"     : host,
                        "domain"   : domain
                        "password" : self.password,
                        "host"     : host,
                        "domain"   : domain
@@ -520,11 +954,11 @@ class DDNSProviderNamecheap(DDNSProvider):
                output = response.read()
 
                # Handle success messages.
                output = response.read()
 
                # Handle success messages.
-               if self.parse_xml(output, "IP") == self.get_address("ipv4"):
+               if self.get_xml_tag_value(output, "IP") == address:
                        return
 
                # Handle error codes.
                        return
 
                # Handle error codes.
-               errorcode = self.parse_xml(output, "ResponseNumber")
+               errorcode = self.get_xml_tag_value(output, "ResponseNumber")
 
                if errorcode == "304156":
                        raise DDNSAuthenticationError
 
                if errorcode == "304156":
                        raise DDNSAuthenticationError
@@ -539,10 +973,11 @@ class DDNSProviderNamecheap(DDNSProvider):
                raise DDNSUpdateError
 
 
                raise DDNSUpdateError
 
 
-class DDNSProviderNOIP(DDNSProviderDynDNS):
-       handle  = "no-ip.com"
-       name    = "No-IP"
-       website = "http://www.no-ip.com/"
+class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "no-ip.com"
+       name      = "No-IP"
+       website   = "http://www.no-ip.com/"
+       protocols = ("ipv4",)
 
        # Information about the format of the HTTP request is to be found
        # here: http://www.no-ip.com/integrate/request and
 
        # Information about the format of the HTTP request is to be found
        # here: http://www.no-ip.com/integrate/request and
@@ -550,19 +985,84 @@ class DDNSProviderNOIP(DDNSProviderDynDNS):
 
        url = "http://dynupdate.no-ip.com/nic/update"
 
 
        url = "http://dynupdate.no-ip.com/nic/update"
 
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
+               assert proto == "ipv4"
+
                data = {
                        "hostname" : self.hostname,
                data = {
                        "hostname" : self.hostname,
-                       "address"  : self.get_address("ipv4"),
+                       "address"  : self.get_address(proto),
+               }
+
+               return data
+
+
+class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "nsupdate.info"
+       name      = "nsupdate.info"
+       website   = "http://nsupdate.info/"
+       protocols = ("ipv6", "ipv4",)
+
+       # Information about the format of the HTTP request can be found
+       # after login on the provider user interface and here:
+       # http://nsupdateinfo.readthedocs.org/en/latest/user.html
+
+       # TODO nsupdate.info can actually do this, but the functionality
+       # has not been implemented here, yet.
+       can_remove_records = False
+
+       # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
+       # and for the password a so called secret.
+       @property
+       def username(self):
+               return self.get("hostname")
+
+       @property
+       def password(self):
+               return self.token or self.get("secret")
+
+       @property
+       def url(self):
+               # The update URL is different by the used protocol.
+               if self.proto == "ipv4":
+                       return "https://ipv4.nsupdate.info/nic/update"
+               elif self.proto == "ipv6":
+                       return "https://ipv6.nsupdate.info/nic/update"
+               else:
+                       raise DDNSUpdateError(_("Invalid protocol has been given"))
+
+       def prepare_request_data(self, proto):
+               data = {
+                       "myip" : self.get_address(proto),
                }
 
                return data
 
 
                }
 
                return data
 
 
-class DDNSProviderOVH(DDNSProviderDynDNS):
-       handle  = "ovh.com"
-       name    = "OVH"
-       website = "http://www.ovh.com/"
+class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "opendns.com"
+       name      = "OpenDNS"
+       website   = "http://www.opendns.com"
+
+       # Detailed information about the update request and possible
+       # response codes can be obtained from here:
+       # https://support.opendns.com/entries/23891440
+
+       url = "https://updates.opendns.com/nic/update"
+
+       def prepare_request_data(self, proto):
+               data = {
+                       "hostname" : self.hostname,
+                       "myip"     : self.get_address(proto),
+               }
+
+               return data
+
+
+class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "ovh.com"
+       name      = "OVH"
+       website   = "http://www.ovh.com/"
+       protocols = ("ipv4",)
 
        # OVH only provides very limited information about how to
        # update a DynDNS host. They only provide the update url
 
        # OVH only provides very limited information about how to
        # update a DynDNS host. They only provide the update url
@@ -572,8 +1072,8 @@ class DDNSProviderOVH(DDNSProviderDynDNS):
 
        url = "https://www.ovh.com/nic/update"
 
 
        url = "https://www.ovh.com/nic/update"
 
-       def _prepare_request_data(self):
-               data = DDNSProviderDynDNS._prepare_request_data(self)
+       def prepare_request_data(self, proto):
+               data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
                data.update({
                        "system" : "dyndns",
                })
                data.update({
                        "system" : "dyndns",
                })
@@ -591,6 +1091,7 @@ class DDNSProviderRegfish(DDNSProvider):
        # https://www.regfish.de/domains/dyndns/dokumentation
 
        url = "https://dyndns.regfish.de/"
        # https://www.regfish.de/domains/dyndns/dokumentation
 
        url = "https://dyndns.regfish.de/"
+       can_remove_records = False
 
        def update(self):
                data = {
 
        def update(self):
                data = {
@@ -652,15 +1153,16 @@ class DDNSProviderRegfish(DDNSProvider):
                raise DDNSUpdateError
 
 
                raise DDNSUpdateError
 
 
-class DDNSProviderSelfhost(DDNSProviderDynDNS):
+class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
        handle    = "selfhost.de"
        name      = "Selfhost.de"
        website   = "http://www.selfhost.de/"
        handle    = "selfhost.de"
        name      = "Selfhost.de"
        website   = "http://www.selfhost.de/"
+       protocols = ("ipv4",)
 
        url = "https://carol.selfhost.de/nic/update"
 
 
        url = "https://carol.selfhost.de/nic/update"
 
-       def _prepare_request_data(self):
-               data = DDNSProviderDynDNS._prepare_request_data(self)
+       def prepare_request_data(self, proto):
+               data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
                data.update({
                        "hostname" : "1",
                })
                data.update({
                        "hostname" : "1",
                })
@@ -668,10 +1170,10 @@ class DDNSProviderSelfhost(DDNSProviderDynDNS):
                return data
 
 
                return data
 
 
-class DDNSProviderSPDNS(DDNSProviderDynDNS):
-       handle  = "spdns.org"
-       name    = "SPDNS"
-       website = "http://spdns.org/"
+class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "spdns.org"
+       name      = "SPDNS"
+       website   = "http://spdns.org/"
 
        # Detailed information about request and response codes are provided
        # by the vendor. They are using almost the same mechanism and status
 
        # Detailed information about request and response codes are provided
        # by the vendor. They are using almost the same mechanism and status
@@ -682,11 +1184,20 @@ class DDNSProviderSPDNS(DDNSProviderDynDNS):
 
        url = "https://update.spdns.de/nic/update"
 
 
        url = "https://update.spdns.de/nic/update"
 
+       @property
+       def username(self):
+               return self.get("username") or self.hostname
+
+       @property
+       def password(self):
+               return self.get("username") or self.token
+
 
 
-class DDNSProviderStrato(DDNSProviderDynDNS):
-       handle  = "strato.com"
-       name    = "Strato AG"
-       website = "http:/www.strato.com/"
+class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "strato.com"
+       name      = "Strato AG"
+       website   = "http:/www.strato.com/"
+       protocols = ("ipv4",)
 
        # Information about the request and response can be obtained here:
        # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
 
        # Information about the request and response can be obtained here:
        # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
@@ -694,10 +1205,11 @@ class DDNSProviderStrato(DDNSProviderDynDNS):
        url = "https://dyndns.strato.com/nic/update"
 
 
        url = "https://dyndns.strato.com/nic/update"
 
 
-class DDNSProviderTwoDNS(DDNSProviderDynDNS):
-       handle  = "twodns.de"
-       name    = "TwoDNS"
-       website = "http://www.twodns.de"
+class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "twodns.de"
+       name      = "TwoDNS"
+       website   = "http://www.twodns.de"
+       protocols = ("ipv4",)
 
        # Detailed information about the request can be found here
        # http://twodns.de/en/faqs
 
        # Detailed information about the request can be found here
        # http://twodns.de/en/faqs
@@ -705,19 +1217,22 @@ class DDNSProviderTwoDNS(DDNSProviderDynDNS):
 
        url = "https://update.twodns.de/update"
 
 
        url = "https://update.twodns.de/update"
 
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
+               assert proto == "ipv4"
+
                data = {
                data = {
-                       "ip" : self.get_address("ipv4"),
+                       "ip"       : self.get_address(proto),
                        "hostname" : self.hostname
                }
 
                return data
 
 
                        "hostname" : self.hostname
                }
 
                return data
 
 
-class DDNSProviderUdmedia(DDNSProviderDynDNS):
-       handle  = "udmedia.de"
-       name    = "Udmedia GmbH"
-       website = "http://www.udmedia.de"
+class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "udmedia.de"
+       name      = "Udmedia GmbH"
+       website   = "http://www.udmedia.de"
+       protocols = ("ipv4",)
 
        # Information about the request can be found here
        # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
 
        # Information about the request can be found here
        # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
@@ -725,7 +1240,7 @@ class DDNSProviderUdmedia(DDNSProviderDynDNS):
        url = "https://www.udmedia.de/nic/update"
 
 
        url = "https://www.udmedia.de/nic/update"
 
 
-class DDNSProviderVariomedia(DDNSProviderDynDNS):
+class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
        handle    = "variomedia.de"
        name      = "Variomedia"
        website   = "http://www.variomedia.de/"
        handle    = "variomedia.de"
        name      = "Variomedia"
        website   = "http://www.variomedia.de/"
@@ -736,23 +1251,20 @@ class DDNSProviderVariomedia(DDNSProviderDynDNS):
 
        url = "https://dyndns.variomedia.de/nic/update"
 
 
        url = "https://dyndns.variomedia.de/nic/update"
 
-       @property
-       def proto(self):
-               return self.get("proto")
-
-       def _prepare_request_data(self):
+       def prepare_request_data(self, proto):
                data = {
                        "hostname" : self.hostname,
                data = {
                        "hostname" : self.hostname,
-                       "myip"     : self.get_address(self.proto)
+                       "myip"     : self.get_address(proto),
                }
 
                return data
 
 
                }
 
                return data
 
 
-class DDNSProviderZoneedit(DDNSProvider):
-       handle  = "zoneedit.com"
-       name    = "Zoneedit"
-       website = "http://www.zoneedit.com"
+class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
+       handle    = "zoneedit.com"
+       name      = "Zoneedit"
+       website   = "http://www.zoneedit.com"
+       protocols = ("ipv4",)
 
        # Detailed information about the request and the response codes can be
        # obtained here:
 
        # Detailed information about the request and the response codes can be
        # obtained here:
@@ -761,13 +1273,9 @@ class DDNSProviderZoneedit(DDNSProvider):
 
        url = "https://dynamic.zoneedit.com/auth/dynamic.html"
 
 
        url = "https://dynamic.zoneedit.com/auth/dynamic.html"
 
-       @property
-       def proto(self):
-               return self.get("proto")
-
-       def update(self):
+       def update_protocol(self, proto):
                data = {
                data = {
-                       "dnsto" : self.get_address(self.proto),
+                       "dnsto" : self.get_address(proto),
                        "host"  : self.hostname
                }
 
                        "host"  : self.hostname
                }
 
@@ -792,3 +1300,50 @@ class DDNSProviderZoneedit(DDNSProvider):
 
                # If we got here, some other update error happened.
                raise DDNSUpdateError
 
                # If we got here, some other update error happened.
                raise DDNSUpdateError
+
+
+class DDNSProviderZZZZ(DDNSProvider):
+       handle    = "zzzz.io"
+       name      = "zzzz"
+       website   = "https://zzzz.io"
+       protocols = ("ipv6", "ipv4",)
+
+       # Detailed information about the update request can be found here:
+       # https://zzzz.io/faq/
+
+       # Details about the possible response codes have been provided in the bugtracker:
+       # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
+
+       url = "https://zzzz.io/api/v1/update"
+       can_remove_records = False
+
+       def update_protocol(self, proto):
+               data = {
+                       "ip"    : self.get_address(proto),
+                       "token" : self.token,
+               }
+
+               if proto == "ipv6":
+                       data["type"] = "aaaa"
+
+               # zzzz uses the host from the full hostname as part
+               # of the update url.
+               host, domain = self.hostname.split(".", 1)
+
+               # Add host value to the update url.
+               url = "%s/%s" % (self.url, host)
+
+               # Send update to the server.
+               try:
+                       response = self.send_request(url, data=data)
+
+               # Handle error codes.
+               except DDNSNotFound:
+                       raise DDNSRequestError(_("Invalid hostname specified"))
+
+               # Handle success messages.
+               if response.code == 200:
+                       return
+
+               # If we got here, some other update error happened.
+               raise DDNSUpdateError