X-Git-Url: http://git.ipfire.org/?p=oddments%2Fddns.git;a=blobdiff_plain;f=src%2Fddns%2Fproviders.py;h=f1fed2265c6c62510f3207039cf26f007fa3deca;hp=d0c01e7867b0a937726aeb75fcd4b09466194046;hb=HEAD;hpb=571271bc73ac001ab798ed94382a5b1a2e493813 diff --git a/src/ddns/providers.py b/src/ddns/providers.py index d0c01e7..59f9665 100644 --- a/src/ddns/providers.py +++ b/src/ddns/providers.py @@ -21,6 +21,7 @@ import datetime import logging +import json import os import subprocess import urllib.request @@ -73,6 +74,10 @@ class DDNSProvider(object): # Required to remove AAAA records if IPv6 is absent again. can_remove_records = True + # True if the provider supports authentication via a random + # generated token instead of username and password. + supports_token_auth = True + @staticmethod def supported(): """ @@ -352,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, @@ -370,7 +379,7 @@ class DDNSProtocolDynDNS2(object): 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"): @@ -434,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 @@ -447,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"): @@ -464,6 +474,8 @@ class DDNSProviderBindNsupdate(DDNSProvider): DEFAULT_TTL = 60 + supports_token_auth = False + @staticmethod def supported(): # Search if the nsupdate utility is available @@ -536,7 +548,7 @@ class DDNSProviderBindNsupdate(DDNSProvider): logger.debug(" %s" % line) - return "\n".join(scriptlet) + return "\n".join(scriptlet).encode() class DDNSProviderChangeIP(DDNSProvider): @@ -550,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 = { @@ -614,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 = { @@ -642,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": @@ -678,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 = { @@ -712,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 = { @@ -723,7 +738,7 @@ class DDNSProviderDNSpark(DDNSProvider): 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"): @@ -760,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 = { @@ -772,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() @@ -804,16 +820,63 @@ class DDNSProviderDtDNS(DDNSProvider): raise DDNSUpdateError -class DDNSProviderDuckDNS(DDNSProtocolDynDNS2, DDNSProvider): +class DDNSProviderDuckDNS(DDNSProvider): handle = "duckdns.org" name = "Duck DNS" website = "http://www.duckdns.org/" - protocols = ("ipv4",) + protocols = ("ipv6", "ipv4",) # Information about the format of the request is to be found - # https://www.duckdns.org/install.jsp + # 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) - url = "https://www.duckdns.org/nic/update" + # 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): @@ -826,7 +889,7 @@ class DDNSProviderDyFi(DDNSProtocolDynDNS2, DDNSProvider): # https://www.dy.fi/page/clients?lang=en # https://www.dy.fi/page/specification?lang=en - url = "http://www.dy.fi/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 @@ -870,6 +933,7 @@ class DDNSProviderDynUp(DDNSProvider): url = "https://dynup.de/dyn.php" can_remove_records = False + supports_token_auth = False def update_protocol(self, proto): data = { @@ -883,7 +947,7 @@ class DDNSProviderDynUp(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() @@ -933,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 = { @@ -945,7 +1011,7 @@ class DDNSProviderEasyDNS(DDNSProvider): 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() @@ -979,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): @@ -988,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 = { @@ -1007,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"): @@ -1039,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 = { @@ -1053,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": @@ -1081,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 = { @@ -1119,25 +1189,23 @@ 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. @@ -1150,6 +1218,71 @@ class DDNSProviderFreeDNSAfraidOrg(DDNSProvider): 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" @@ -1188,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" @@ -1203,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) @@ -1286,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. @@ -1305,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: @@ -1337,7 +1509,7 @@ class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider): # here: http://www.noip.com/integrate/request and # here: http://www.noip.com/integrate/response - url = "http://dynupdate.noip.com/nic/update" + url = "https://dynupdate.noip.com/nic/update" def prepare_request_data(self, proto): assert proto == "ipv4" @@ -1379,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 @@ -1455,6 +1629,7 @@ class DDNSProviderRegfish(DDNSProvider): url = "https://dyndns.regfish.de/" can_remove_records = False + supports_token_auth = True def update(self): data = { @@ -1493,7 +1668,7 @@ class DDNSProviderRegfish(DDNSProvider): 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: @@ -1551,6 +1726,7 @@ class DDNSProviderServercow(DDNSProvider): url = "https://www.servercow.de/dnsupdate/update.php" can_remove_records = False + supports_token_auth = False def update_protocol(self, proto): data = { @@ -1564,7 +1740,7 @@ class DDNSProviderServercow(DDNSProvider): response = self.send_request(self.url, data=data) # Read response - output = response.read() + output = response.read().decode() # Server responds with OK if update was successful if output.startswith("OK"): @@ -1592,6 +1768,8 @@ class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider): url = "https://update.spdyn.de/nic/update" + supports_token_auth = True + @property def username(self): return self.get("username") or self.hostname @@ -1695,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 @@ -1712,7 +1892,7 @@ class DDNSProviderZoneedit(DDNSProvider): 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("