X-Git-Url: http://git.ipfire.org/?p=oddments%2Fddns.git;a=blobdiff_plain;f=src%2Fddns%2Fproviders.py;h=f1fed2265c6c62510f3207039cf26f007fa3deca;hp=5f4dac87eab1cbac550876ca15bfc3a67dc45976;hb=HEAD;hpb=fc91be92269f9b4662b6586cfde73ee04b061367 diff --git a/src/ddns/providers.py b/src/ddns/providers.py index 5f4dac8..59f9665 100644 --- a/src/ddns/providers.py +++ b/src/ddns/providers.py @@ -1,8 +1,8 @@ -#!/usr/bin/python +#!/usr/bin/python3 ############################################################################### # # # ddns - A dynamic DNS client for IPFire # -# Copyright (C) 2012 IPFire development team # +# Copyright (C) 2012-2017 IPFire development team # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # @@ -21,12 +21,15 @@ import datetime import logging +import json import os import subprocess -import urllib2 +import urllib.request +import urllib.error +import urllib.parse import xml.dom.minidom -from i18n import _ +from .i18n import _ # Import all possible exception types. from .errors import * @@ -71,23 +74,9 @@ class DDNSProvider(object): # 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 + # True if the provider supports authentication via a random + # generated token instead of username and password. + supports_token_auth = True @staticmethod def supported(): @@ -105,11 +94,23 @@ class DDNSProvider(object): self.settings = self.DEFAULT_SETTINGS.copy() self.settings.update(settings) + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + + if not all((cls.handle, cls.name, cls.website)): + raise DDNSError(_("Provider is not properly configured")) + + assert cls.handle not in _providers, \ + "Provider '%s' has already been registered" % cls.handle + + # Register class + _providers[cls.handle] = cls + def __repr__(self): return "" % (self.name, self.handle) def __cmp__(self, other): - return cmp(self.hostname, other.hostname) + return (lambda a, b: (a > b)-(a < b))(self.hostname, other.hostname) @property def db(self): @@ -176,8 +177,8 @@ class DDNSProvider(object): 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 }) + 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): @@ -192,7 +193,7 @@ class DDNSProvider(object): def remove_protocol(self, proto): if not self.can_remove_records: - raise RuntimeError, "can_remove_records is enabled, but remove_protocol() not implemented" + raise RuntimeError("can_remove_records is enabled, but remove_protocol() not implemented") raise NotImplementedError @@ -200,23 +201,21 @@ class DDNSProvider(object): 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 }) + 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 }) + 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 }) + logger.debug(_("No update required for %(hostname)s (%(provider)s)") % + {"hostname": self.hostname, "provider": self.name}) return False @@ -234,8 +233,7 @@ class DDNSProvider(object): # If there is no holdoff time, we won't update ever again. if self.holdoff_failure_days is None: - logger.warning(_("An update has not been performed because earlier updates failed for %s") \ - % self.hostname) + logger.warning(_("An update has not been performed because earlier updates failed for %s") % self.hostname) logger.warning(_("There will be no retries")) return True @@ -248,8 +246,7 @@ class DDNSProvider(object): if now < holdoff_end: failure_message = self.db.last_update_failure_message(self.hostname) - logger.warning(_("An update has not been performed because earlier updates failed for %s") \ - % self.hostname) + logger.warning(_("An update has not been performed because earlier updates failed for %s") % self.hostname) if failure_message: logger.warning(_("Last failure message:")) @@ -315,8 +312,8 @@ class DDNSProvider(object): 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)) + logger.debug("Updates for %s are held off until %s" % + (self.hostname, holdoff_end)) return False def send_request(self, *args, **kwargs): @@ -360,6 +357,10 @@ class DDNSProtocolDynDNS2(object): # The DynDNS protocol version 2 does not allow to remove records can_remove_records = False + # The DynDNS protocol version 2 only supports authentication via + # username and password. + supports_token_auth = False + def prepare_request_data(self, proto): data = { "hostname" : self.hostname, @@ -375,11 +376,10 @@ class DDNSProtocolDynDNS2(object): 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) + response = DDNSProvider.send_request(self, self.url, data=data, username=self.username, password=self.password) # Get the full response message. - output = response.read() + output = response.read().decode() # Handle success messages. if output.startswith("good") or output.startswith("nochg"): @@ -391,15 +391,17 @@ class DDNSProtocolDynDNS2(object): elif output == "abuse": raise DDNSAbuseError elif output == "notfqdn": - raise DDNSRequestError(_("No valid FQDN was given.")) + raise DDNSRequestError(_("No valid FQDN was given")) elif output == "nohost": - raise DDNSRequestError(_("Specified host does not exist.")) + raise DDNSRequestError(_("Specified host does not exist")) elif output == "911": raise DDNSInternalServerError elif output == "dnserr": - raise DDNSInternalServerError(_("DNS error encountered.")) + raise DDNSInternalServerError(_("DNS error encountered")) elif output == "badagent": raise DDNSBlockedError + elif output == "badip": + raise DDNSBlockedError # If we got here, some other update error happened. raise DDNSUpdateError(_("Server response: %s") % output) @@ -411,7 +413,7 @@ class DDNSResponseParserXML(object): 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. @@ -441,12 +443,13 @@ class DDNSProviderAllInkl(DDNSProvider): protocols = ("ipv4",) # There are only information provided by the vendor how to - # perform an update on a FRITZ Box. Grab requried informations + # perform an update on a FRITZ Box. Grab required information # from the net. # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/ - url = "http://dyndns.kasserver.com" + url = "https://dyndns.kasserver.com" can_remove_records = False + supports_token_auth = False def update(self): # There is no additional data required so we directly can @@ -454,7 +457,7 @@ class DDNSProviderAllInkl(DDNSProvider): response = self.send_request(self.url, username=self.username, password=self.password) # Get the full response message. - output = response.read() + output = response.read().decode() # Handle success messages. if output.startswith("good") or output.startswith("nochg"): @@ -471,6 +474,8 @@ class DDNSProviderBindNsupdate(DDNSProvider): DEFAULT_TTL = 60 + supports_token_auth = False + @staticmethod def supported(): # Search if the nsupdate utility is available @@ -492,9 +497,7 @@ class DDNSProviderBindNsupdate(DDNSProvider): # -t sets the timeout command = ["nsupdate", "-v", "-t", "60"] - p = subprocess.Popen(command, shell=True, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - ) + p = subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate(scriptlet) if p.returncode == 0: @@ -545,7 +548,7 @@ class DDNSProviderBindNsupdate(DDNSProvider): logger.debug(" %s" % line) - return "\n".join(scriptlet) + return "\n".join(scriptlet).encode() class DDNSProviderChangeIP(DDNSProvider): @@ -559,6 +562,7 @@ class DDNSProviderChangeIP(DDNSProvider): url = "https://nic.changeip.com/nic/update" can_remove_records = False + supports_token_auth = False def update_protocol(self, proto): data = { @@ -568,11 +572,10 @@ class DDNSProviderChangeIP(DDNSProvider): # Send update to the server. try: - response = self.send_request(self.url, username=self.username, password=self.password, - data=data) + response = self.send_request(self.url, username=self.username, password=self.password, data=data) # Handle error codes. - except urllib2.HTTPError, e: + except urllib.error.HTTPError as e: if e.code == 422: raise DDNSRequestError(_("Domain not found.")) @@ -624,8 +627,9 @@ class DDNSProviderDDNSS(DDNSProvider): # http://www.ddnss.de/info.php # http://www.megacomputing.de/2014/08/dyndns-service-response-time/#more-919 - url = "http://www.ddnss.de/upd.php" + url = "https://www.ddnss.de/upd.php" can_remove_records = False + supports_token_auth = False def update_protocol(self, proto): data = { @@ -652,10 +656,8 @@ class DDNSProviderDDNSS(DDNSProvider): response = self.send_request(self.url, data=data) # This provider sends the response code as part of the header. - header = response.info() - # Get status information from the header. - output = header.getheader('ddnss-response') + output = response.getheader('ddnss-response') # Handle success messages. if output == "good" or output == "nochg": @@ -665,15 +667,15 @@ class DDNSProviderDDNSS(DDNSProvider): if output == "badauth": raise DDNSAuthenticationError elif output == "notfqdn": - raise DDNSRequestError(_("No valid FQDN was given.")) + raise DDNSRequestError(_("No valid FQDN was given")) elif output == "nohost": - raise DDNSRequestError(_("Specified host does not exist.")) + raise DDNSRequestError(_("Specified host does not exist")) elif output == "911": raise DDNSInternalServerError elif output == "dnserr": - raise DDNSInternalServerError(_("DNS error encountered.")) + raise DDNSInternalServerError(_("DNS error encountered")) elif output == "disabled": - raise DDNSRequestError(_("Account disabled or locked.")) + raise DDNSRequestError(_("Account disabled or locked")) # If we got here, some other update error happened. raise DDNSUpdateError @@ -688,8 +690,10 @@ class DDNSProviderDHS(DDNSProvider): # No information about the used update api provided on webpage, # grabed from source code of ez-ipudate. - url = "http://members.dhs.org/nic/hosts" + # Provider currently does not support TLS 1.2. + url = "https://members.dhs.org/nic/hosts" can_remove_records = False + supports_token_auth = False def update_protocol(self, proto): data = { @@ -701,8 +705,7 @@ class DDNSProviderDHS(DDNSProvider): } # Send update to the server. - response = self.send_request(self.url, username=self.username, password=self.password, - data=data) + response = self.send_request(self.url, username=self.username, password=self.password, data=data) # Handle success messages. if response.code == 200: @@ -723,6 +726,7 @@ class DDNSProviderDNSpark(DDNSProvider): url = "https://control.dnspark.com/api/dynamic/update.php" can_remove_records = False + supports_token_auth = False def update_protocol(self, proto): data = { @@ -731,11 +735,10 @@ class DDNSProviderDNSpark(DDNSProvider): } # Send update to the server. - response = self.send_request(self.url, username=self.username, password=self.password, - data=data) + response = self.send_request(self.url, username=self.username, password=self.password, data=data) # Get the full response message. - output = response.read() + output = response.read().decode() # Handle success messages. if output.startswith("ok") or output.startswith("nochange"): @@ -749,13 +752,13 @@ class DDNSProviderDNSpark(DDNSProvider): elif output == "blocked": raise DDNSBlockedError elif output == "nofqdn": - raise DDNSRequestError(_("No valid FQDN was given.")) + raise DDNSRequestError(_("No valid FQDN was given")) elif output == "nohost": - raise DDNSRequestError(_("Invalid hostname specified.")) + raise DDNSRequestError(_("Invalid hostname specified")) elif output == "notdyn": - raise DDNSRequestError(_("Hostname not marked as a dynamic host.")) + raise DDNSRequestError(_("Hostname not marked as a dynamic host")) elif output == "invalid": - raise DDNSRequestError(_("Invalid IP address has been sent.")) + raise DDNSRequestError(_("Invalid IP address has been sent")) # If we got here, some other update error happened. raise DDNSUpdateError @@ -772,6 +775,7 @@ class DDNSProviderDtDNS(DDNSProvider): url = "https://www.dtdns.com/api/autodns.cfm" can_remove_records = False + supports_token_auth = False def update_protocol(self, proto): data = { @@ -784,7 +788,7 @@ class DDNSProviderDtDNS(DDNSProvider): response = self.send_request(self.url, data=data) # Get the full response message. - output = response.read() + output = response.read().decode() # Remove all leading and trailing whitespace. output = output.strip() @@ -795,37 +799,102 @@ class DDNSProviderDtDNS(DDNSProvider): # Handle error codes. if output == "No hostname to update was supplied.": - raise DDNSRequestError(_("No hostname specified.")) + raise DDNSRequestError(_("No hostname specified")) elif output == "The hostname you supplied is not valid.": - raise DDNSRequestError(_("Invalid hostname specified.")) + raise DDNSRequestError(_("Invalid hostname specified")) elif output == "The password you supplied is not valid.": raise DDNSAuthenticationError elif output == "Administration has disabled this account.": - raise DDNSRequestError(_("Account has been disabled.")) + raise DDNSRequestError(_("Account has been disabled")) elif output == "Illegal character in IP.": - raise DDNSRequestError(_("Invalid IP address has been sent.")) + raise DDNSRequestError(_("Invalid IP address has been sent")) elif output == "Too many failed requests.": - raise DDNSRequestError(_("Too many failed requests.")) + raise DDNSRequestError(_("Too many failed requests")) # If we got here, some other update error happened. raise DDNSUpdateError -class DDNSProviderDuckDNS(DDNSProtocolDynDNS2, DDNSProvider): +class DDNSProviderDuckDNS(DDNSProvider): handle = "duckdns.org" name = "Duck DNS" website = "http://www.duckdns.org/" + protocols = ("ipv6", "ipv4",) + + # Information about the format of the request is to be found + # https://www.duckdns.org/spec.jsp + + url = "https://www.duckdns.org/update" + can_remove_records = False + supports_token_auth = True + + def update(self): + # Raise an error if no auth details are given. + if not self.token: + raise DDNSConfigurationError + + data = { + "domains" : self.hostname, + "token" : self.token, + } + + # Check if we update an IPv4 address. + address4 = self.get_address("ipv4") + if address4: + data["ip"] = address4 + + # Check if we update an IPv6 address. + address6 = self.get_address("ipv6") + if address6: + data["ipv6"] = address6 + + # Raise an error if no address is given. + if "ip" not in data and "ipv6" not in data: + raise DDNSConfigurationError + + # Send update to the server. + response = self.send_request(self.url, data=data) + + # Get the full response message. + output = response.read().decode() + + # Remove all leading and trailing whitespace. + output = output.strip() + + # Handle success messages. + if output == "OK": + return + + # The provider does not give detailed information + # if the update fails. Only a "KO" will be sent back. + if output == "KO": + raise DDNSUpdateError + + # If we got here, some other update error happened. + raise DDNSUpdateError + + +class DDNSProviderDyFi(DDNSProtocolDynDNS2, DDNSProvider): + handle = "dy.fi" + name = "dy.fi" + website = "https://www.dy.fi/" protocols = ("ipv4",) # Information about the format of the request is to be found - # https://www.duckdns.org/install.jsp + # https://www.dy.fi/page/clients?lang=en + # https://www.dy.fi/page/specification?lang=en - url = "https://www.duckdns.org/nic/update" + url = "https://www.dy.fi/nic/update" + + # Please only send automatic updates when your IP address changes, + # or once per 5 to 6 days to refresh the address mapping (they will + # expire if not refreshed within 7 days). + holdoff_days = 6 class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider): @@ -841,6 +910,56 @@ class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider): url = "https://members.dyndns.org/nic/update" +class DDNSProviderDomainOffensive(DDNSProtocolDynDNS2, DDNSProvider): + handle = "do.de" + name = "Domain-Offensive" + website = "https://www.do.de/" + protocols = ("ipv6", "ipv4") + + # Detailed information about the request and response codes + # are available on the providers webpage. + # https://www.do.de/wiki/FlexDNS_-_Entwickler + + url = "https://ddns.do.de/" + +class DDNSProviderDynUp(DDNSProvider): + handle = "dynup.de" + name = "DynUp.DE" + website = "http://dynup.de/" + protocols = ("ipv4",) + + # Information about the format of the HTTPS request is to be found + # https://dyndnsfree.de/user/hilfe.php + + url = "https://dynup.de/dyn.php" + can_remove_records = False + supports_token_auth = False + + def update_protocol(self, proto): + data = { + "username" : self.username, + "password" : self.password, + "hostname" : self.hostname, + "print" : '1', + } + + # Send update to the server. + response = self.send_request(self.url, data=data) + + # Get the full response message. + output = response.read().decode() + + # Remove all leading and trailing whitespace. + output = output.strip() + + # Handle success messages. + if output.startswith("I:OK"): + return + + # If we got here, some other update error happened. + raise DDNSUpdateError + + class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider): handle = "dynu.com" name = "Dynu" @@ -878,7 +997,9 @@ class DDNSProviderEasyDNS(DDNSProvider): # (API 1.3) are available on the providers webpage. # https://fusion.easydns.com/index.php?/Knowledgebase/Article/View/102/7/dynamic-dns - url = "http://api.cp.easydns.com/dyn/tomato.php" + url = "https://api.cp.easydns.com/dyn/tomato.php" + + supports_token_auth = False def update_protocol(self, proto): data = { @@ -887,11 +1008,10 @@ class DDNSProviderEasyDNS(DDNSProvider): } # 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, username=self.username, password=self.password) # Get the full response message. - output = response.read() + output = response.read().decode() # Remove all leading and trailing whitespace. output = output.strip() @@ -905,13 +1025,13 @@ class DDNSProviderEasyDNS(DDNSProvider): raise DDNSAuthenticationError elif output.startswith("NOSERVICE"): - raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain.")) + raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain")) elif output.startswith("ILLEGAL INPUT"): - raise DDNSRequestError(_("Invalid data has been sent.")) + raise DDNSRequestError(_("Invalid data has been sent")) elif output.startswith("TOOSOON"): - raise DDNSRequestError(_("Too frequent update requests have been sent.")) + raise DDNSRequestError(_("Too frequent update requests have been sent")) # If we got here, some other update error happened. raise DDNSUpdateError @@ -925,7 +1045,8 @@ class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider): # https://www.domopoli.de/?page=howto#DynDns_start - url = "http://dyndns.domopoli.de/nic/update" + # This provider does not support TLS 1.2. + url = "https://dyndns.domopoli.de/nic/update" class DDNSProviderDynsNet(DDNSProvider): @@ -934,12 +1055,13 @@ class DDNSProviderDynsNet(DDNSProvider): website = "http://www.dyns.net/" protocols = ("ipv4",) can_remove_records = False + supports_token_auth = 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" + url = "https://www.dyns.net/postscript011.php" def update_protocol(self, proto): data = { @@ -953,7 +1075,7 @@ class DDNSProviderDynsNet(DDNSProvider): response = self.send_request(self.url, data=data) # Get the full response message. - output = response.read() + output = response.read().decode() # Handle success messages. if output.startswith("200"): @@ -961,16 +1083,16 @@ class DDNSProviderDynsNet(DDNSProvider): # Handle error codes. if output.startswith("400"): - raise DDNSRequestError(_("Malformed request has been sent.")) + raise DDNSRequestError(_("Malformed request has been sent")) elif output.startswith("401"): raise DDNSAuthenticationError elif output.startswith("402"): - raise DDNSRequestError(_("Too frequent update requests have been sent.")) + raise DDNSRequestError(_("Too frequent update requests have been sent")) elif output.startswith("403"): raise DDNSInternalServerError # If we got here, some other update error happened. - raise DDNSUpdateError(_("Server response: %s") % output) + raise DDNSUpdateError(_("Server response: %s") % output) class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider): @@ -985,6 +1107,7 @@ class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider): url = "https://dynamic.name-services.com/interface.asp" can_remove_records = False + supports_token_auth = False def update_protocol(self, proto): data = { @@ -999,7 +1122,7 @@ class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider): response = self.send_request(self.url, data=data) # Get the full response message. - output = response.read() + output = response.read().decode() # Handle success messages. if self.get_xml_tag_value(output, "ErrCount") == "0": @@ -1011,7 +1134,7 @@ class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider): if errorcode == "304155": raise DDNSAuthenticationError elif errorcode == "304153": - raise DDNSRequestError(_("Domain not found.")) + raise DDNSRequestError(_("Domain not found")) # If we got here, some other update error happened. raise DDNSUpdateError @@ -1027,6 +1150,7 @@ class DDNSProviderEntryDNS(DDNSProvider): # here: https://entrydns.net/help url = "https://entrydns.net/records/modify" can_remove_records = False + supports_token_auth = True def update_protocol(self, proto): data = { @@ -1041,7 +1165,7 @@ class DDNSProviderEntryDNS(DDNSProvider): response = self.send_request(url, data=data) # Handle error codes - except urllib2.HTTPError, e: + except urllib.error.HTTPError as e: if e.code == 404: raise DDNSAuthenticationError @@ -1065,37 +1189,125 @@ 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" + url = "https://sync.afraid.org/u/" can_remove_records = False + supports_token_auth = True def update_protocol(self, proto): - data = { - "address" : self.get_address(proto), - } # Add auth token to the update url. - url = "%s?%s" % (self.url, self.token) + url = "%s%s/" % (self.url, self.token) # Send update to the server. - response = self.send_request(url, data=data) + response = self.send_request(url) # Get the full response message. - output = response.read() + output = response.read().decode() # Handle success messages. - if output.startswith("Updated") or "has not changed" in output: + if output.startswith("Updated") or output.startswith("No IP change detected"): return # Handle error codes. if output == "ERROR: Unable to locate this record": raise DDNSAuthenticationError elif "is an invalid IP address" in output: - raise DDNSRequestError(_("Invalid IP address has been sent.")) + raise DDNSRequestError(_("Invalid IP address has been sent")) + + # If we got here, some other update error happened. + raise DDNSUpdateError + + +class DDNSProviderGodaddy(DDNSProvider): + handle = "godaddy.com" + name = "godaddy.com" + website = "https://godaddy.com/" + protocols = ("ipv4",) + + # Information about the format of the HTTP request is to be found + # here: https://developer.godaddy.com/doc/endpoint/domains#/v1/recordReplaceTypeName + url = "https://api.godaddy.com/v1/domains/" + can_remove_records = False + + def update_protocol(self, proto): + # retrieve ip + ip_address = self.get_address(proto) + + # set target url + url = f"{self.url}/{self.hostname}/records/A/@" + + # prepare data + data = json.dumps([{"data": ip_address, "ttl": 600, "name": self.hostname, "type": "A"}]).encode("utf-8") + + # Method requires authentication by special headers. + request = urllib.request.Request(url=url, + data=data, + headers={"Authorization": f"sso-key {self.username}:{self.password}", + "Content-Type": "application/json"}, + method="PUT") + result = urllib.request.urlopen(request) + + # handle success + if result.code == 200: + return + + # handle errors + if result.code == 400: + raise DDNSRequestError(_("Malformed request received.")) + if result.code in (401, 403): + raise DDNSAuthenticationError + if result.code == 404: + raise DDNSRequestError(_("Resource not found.")) + if result.code == 422: + raise DDNSRequestError(_("Record does not fulfill the schema.")) + if result.code == 429: + raise DDNSRequestError(_("API Rate limiting.")) # If we got here, some other update error happened. raise DDNSUpdateError +class DDNSProviderHENet(DDNSProtocolDynDNS2, DDNSProvider): + handle = "he.net" + name = "he.net" + website = "https://he.net" + protocols = ("ipv6", "ipv4",) + + # Detailed information about the update api can be found here. + # http://dns.he.net/docs.html + + url = "https://dyn.dns.he.net/nic/update" + @property + def username(self): + return self.get("hostname") + + + +class DDNSProviderItsdns(DDNSProtocolDynDNS2, DDNSProvider): + handle = "inwx.com" + name = "INWX" + website = "https://www.inwx.com" + protocols = ("ipv6", "ipv4") + + # Information about the format of the HTTP request is to be found + # here: https://www.inwx.com/en/nameserver2/dyndns (requires login) + # Notice: The URL is the same for: inwx.com|de|at|ch|es + + url = "https://dyndns.inwx.com/nic/update" + + +class DDNSProviderItsdns(DDNSProtocolDynDNS2, DDNSProvider): + handle = "itsdns.de" + name = "it's DNS" + website = "http://www.itsdns.de/" + protocols = ("ipv6", "ipv4") + + # Information about the format of the HTTP request is to be found + # here: https://www.itsdns.de/dynupdatehelp.htm + + url = "https://www.itsdns.de/update.php" + + class DDNSProviderJoker(DDNSProtocolDynDNS2, DDNSProvider): handle = "joker.com" name = "Joker.com Dynamic DNS" @@ -1109,6 +1321,52 @@ class DDNSProviderJoker(DDNSProtocolDynDNS2, DDNSProvider): url = "https://svc.joker.com/nic/update" +class DDNSProviderKEYSYSTEMS(DDNSProvider): + handle = "key-systems.net" + name = "dynamicdns.key-systems.net" + website = "https://domaindiscount24.com/" + protocols = ("ipv4",) + + # There are only information provided by the domaindiscount24 how to + # perform an update with HTTP APIs + # https://www.domaindiscount24.com/faq/dynamic-dns + # examples: https://dynamicdns.key-systems.net/update.php?hostname=hostname&password=password&ip=auto + # https://dynamicdns.key-systems.net/update.php?hostname=hostname&password=password&ip=213.x.x.x&mx=213.x.x.x + + url = "https://dynamicdns.key-systems.net/update.php" + can_remove_records = False + supports_token_auth = False + + def update_protocol(self, proto): + address = self.get_address(proto) + data = { + "hostname" : self.hostname, + "password" : self.password, + "ip" : address, + } + + # Send update to the server. + response = self.send_request(self.url, data=data) + + # Get the full response message. + output = response.read().decode() + + # Handle success messages. + if "code = 200" in output: + return + + # Handle error messages. + if "abuse prevention triggered" in output: + raise DDNSAbuseError + elif "invalid password" in output: + raise DDNSAuthenticationError + elif "Authorization failed" in output: + raise DDNSRequestError(_("Invalid hostname specified")) + + # If we got here, some other update error happened. + raise DDNSUpdateError + + class DDNSProviderGoogle(DDNSProtocolDynDNS2, DDNSProvider): handle = "domains.google.com" name = "Google Domains" @@ -1124,35 +1382,27 @@ class DDNSProviderGoogle(DDNSProtocolDynDNS2, DDNSProvider): class DDNSProviderLightningWireLabs(DDNSProvider): handle = "dns.lightningwirelabs.com" name = "Lightning Wire Labs DNS Service" - website = "http://dns.lightningwirelabs.com/" + website = "https://dns.lightningwirelabs.com/" # Information about the format of the HTTPS request is to be found # https://dns.lightningwirelabs.com/knowledge-base/api/ddns + supports_token_auth = True + url = "https://dns.lightningwirelabs.com/update" def update(self): + # Raise an error if no auth details are given. + if not self.token: + raise DDNSConfigurationError + data = { "hostname" : self.hostname, + "token" : self.token, "address6" : self.get_address("ipv6", "-"), "address4" : self.get_address("ipv4", "-"), } - # Check if a token has been set. - if self.token: - data["token"] = self.token - - # Check for username and password. - elif self.username and self.password: - data.update({ - "username" : self.username, - "password" : self.password, - }) - - # Raise an error if no auth details are given. - else: - raise DDNSConfigurationError - # Send update to the server. response = self.send_request(self.url, data=data) @@ -1207,6 +1457,7 @@ class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider): url = "https://dynamicdns.park-your-domain.com/update" can_remove_records = False + supports_token_auth = False def update_protocol(self, proto): # Namecheap requires the hostname splitted into a host and domain part. @@ -1226,7 +1477,7 @@ class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider): response = self.send_request(self.url, data=data) # Get the full response message. - output = response.read() + output = response.read().decode() # Handle success messages. if self.get_xml_tag_value(output, "IP") == address: @@ -1238,9 +1489,9 @@ class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider): if errorcode == "304156": raise DDNSAuthenticationError elif errorcode == "316153": - raise DDNSRequestError(_("Domain not found.")) + raise DDNSRequestError(_("Domain not found")) elif errorcode == "316154": - raise DDNSRequestError(_("Domain not active.")) + raise DDNSRequestError(_("Domain not active")) elif errorcode in ("380098", "380099"): raise DDNSInternalServerError @@ -1250,15 +1501,15 @@ class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider): class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider): handle = "no-ip.com" - name = "No-IP" - website = "http://www.no-ip.com/" + name = "NoIP" + website = "http://www.noip.com/" protocols = ("ipv4",) # Information about the format of the HTTP request is to be found - # here: http://www.no-ip.com/integrate/request and - # here: http://www.no-ip.com/integrate/response + # here: http://www.noip.com/integrate/request and + # here: http://www.noip.com/integrate/response - url = "http://dynupdate.no-ip.com/nic/update" + url = "https://dynupdate.noip.com/nic/update" def prepare_request_data(self, proto): assert proto == "ipv4" @@ -1271,6 +1522,19 @@ class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider): return data +class DDNSProviderNowDNS(DDNSProtocolDynDNS2, DDNSProvider): + handle = "now-dns.com" + name = "NOW-DNS" + website = "http://now-dns.com/" + protocols = ("ipv6", "ipv4") + + # Information about the format of the request is to be found + # but only can be accessed by register an account and login + # https://now-dns.com/?m=api + + url = "https://now-dns.com/update" + + class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider): handle = "nsupdate.info" name = "nsupdate.info" @@ -1287,6 +1551,8 @@ class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider): # has not been implemented here, yet. can_remove_records = False + supports_token_auth = True + # After a failed update, there will be no retries # https://bugzilla.ipfire.org/show_bug.cgi?id=10603 holdoff_failure_days = None @@ -1363,6 +1629,7 @@ class DDNSProviderRegfish(DDNSProvider): url = "https://dyndns.regfish.de/" can_remove_records = False + supports_token_auth = True def update(self): data = { @@ -1380,7 +1647,7 @@ class DDNSProviderRegfish(DDNSProvider): data["ipv4"] = address4 # Raise an error if none address is given. - if not data.has_key("ipv6") and not data.has_key("ipv4"): + if "ipv6" not in data and "ipv4" not in data: raise DDNSConfigurationError # Check if a token has been set. @@ -1390,7 +1657,7 @@ class DDNSProviderRegfish(DDNSProvider): # Raise an error if no token and no useranem and password # are given. elif not self.username and not self.password: - raise DDNSConfigurationError(_("No Auth details specified.")) + raise DDNSConfigurationError(_("No Auth details specified")) # HTTP Basic Auth is only allowed if no token is used. if self.token: @@ -1398,11 +1665,10 @@ class DDNSProviderRegfish(DDNSProvider): response = self.send_request(self.url, data=data) else: # Send update to the server. - response = self.send_request(self.url, username=self.username, password=self.password, - data=data) + response = self.send_request(self.url, username=self.username, password=self.password, data=data) # Get the full response message. - output = response.read() + output = response.read().decode() # Handle success messages. if "100" in output or "101" in output: @@ -1412,11 +1678,11 @@ class DDNSProviderRegfish(DDNSProvider): if "401" or "402" in output: raise DDNSAuthenticationError elif "408" in output: - raise DDNSRequestError(_("Invalid IPv4 address has been sent.")) + raise DDNSRequestError(_("Invalid IPv4 address has been sent")) elif "409" in output: - raise DDNSRequestError(_("Invalid IPv6 address has been sent.")) + raise DDNSRequestError(_("Invalid IPv6 address has been sent")) elif "412" in output: - raise DDNSRequestError(_("No valid FQDN was given.")) + raise DDNSRequestError(_("No valid FQDN was given")) elif "414" in output: raise DDNSInternalServerError @@ -1424,6 +1690,17 @@ class DDNSProviderRegfish(DDNSProvider): raise DDNSUpdateError +class DDNSProviderSchokokeksDNS(DDNSProtocolDynDNS2, DDNSProvider): + handle = "schokokeks.org" + name = "Schokokeks" + website = "http://www.schokokeks.org/" + protocols = ("ipv4",) + + # Information about the format of the request is to be found + # https://wiki.schokokeks.org/DynDNS + url = "https://dyndns.schokokeks.org/nic/update" + + class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider): handle = "selfhost.de" name = "Selfhost.de" @@ -1441,10 +1718,46 @@ class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider): return data +class DDNSProviderServercow(DDNSProvider): + handle = "servercow.de" + name = "servercow.de" + website = "https://servercow.de/" + protocols = ("ipv4", "ipv6") + + url = "https://www.servercow.de/dnsupdate/update.php" + can_remove_records = False + supports_token_auth = False + + def update_protocol(self, proto): + data = { + "ipaddr" : self.get_address(proto), + "hostname" : self.hostname, + "username" : self.username, + "pass" : self.password, + } + + # Send request to provider + response = self.send_request(self.url, data=data) + + # Read response + output = response.read().decode() + + # Server responds with OK if update was successful + if output.startswith("OK"): + return + + # Catch any errors + elif output.startswith("FAILED - Authentication failed"): + raise DDNSAuthenticationError + + # If we got here, some other update error happened + raise DDNSUpdateError(output) + + class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider): handle = "spdns.org" - name = "SPDNS" - website = "http://spdns.org/" + name = "SPDYN" + website = "https://www.spdyn.de/" # Detailed information about request and response codes are provided # by the vendor. They are using almost the same mechanism and status @@ -1453,7 +1766,9 @@ class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider): # http://wiki.securepoint.de/index.php/SPDNS_FAQ # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens - url = "https://update.spdns.de/nic/update" + url = "https://update.spdyn.de/nic/update" + + supports_token_auth = True @property def username(self): @@ -1558,6 +1873,8 @@ class DDNSProviderZoneedit(DDNSProvider): website = "http://www.zoneedit.com" protocols = ("ipv4",) + supports_token_auth = False + # Detailed information about the request and the response codes can be # obtained here: # http://www.zoneedit.com/doc/api/other.html @@ -1572,11 +1889,10 @@ class DDNSProviderZoneedit(DDNSProvider): } # Send update to the server. - response = self.send_request(self.url, username=self.username, password=self.password, - data=data) + response = self.send_request(self.url, username=self.username, password=self.password, data=data) # Get the full response message. - output = response.read() + output = response.read().decode() # Handle success messages. if output.startswith("