X-Git-Url: http://git.ipfire.org/?p=ddns.git;a=blobdiff_plain;f=src%2Fddns%2Fproviders.py;h=f1fed2265c6c62510f3207039cf26f007fa3deca;hp=574e483bfcd96b63082b256a7b2d8df091ed8f9d;hb=ce6e977f0ace7fe468411270ad07f1824a3aeaec;hpb=fb701db9e086a8904e584c122cadfd462580a524 diff --git a/src/ddns/providers.py b/src/ddns/providers.py index 574e483..f1fed22 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 # @@ -23,10 +23,12 @@ import datetime import logging 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,24 +73,6 @@ 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 - @staticmethod def supported(): """ @@ -105,11 +89,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 +172,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 +188,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 +196,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 +228,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 +241,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 +307,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): @@ -375,11 +367,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"): @@ -400,6 +391,8 @@ class DDNSProtocolDynDNS2(object): 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 +404,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. @@ -454,7 +447,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"): @@ -492,9 +485,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: @@ -568,11 +559,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.")) @@ -701,8 +691,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: @@ -731,11 +720,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"): @@ -784,7 +772,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() @@ -828,6 +816,24 @@ class DDNSProviderDuckDNS(DDNSProtocolDynDNS2, DDNSProvider): url = "https://www.duckdns.org/nic/update" +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.dy.fi/page/clients?lang=en + # https://www.dy.fi/page/specification?lang=en + + 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): handle = "dyndns.org" name = "Dyn" @@ -841,6 +847,55 @@ 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 + + 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" @@ -887,11 +942,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() @@ -953,7 +1007,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"): @@ -970,7 +1024,7 @@ class DDNSProviderDynsNet(DDNSProvider): 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): @@ -999,7 +1053,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": @@ -1041,7 +1095,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 @@ -1080,7 +1134,7 @@ class DDNSProviderFreeDNSAfraidOrg(DDNSProvider): response = self.send_request(url, data=data) # 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: @@ -1096,6 +1150,31 @@ class DDNSProviderFreeDNSAfraidOrg(DDNSProvider): raise DDNSUpdateError +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 +1188,51 @@ 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 + + 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,7 +1248,7 @@ 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 @@ -1132,27 +1256,17 @@ class DDNSProviderLightningWireLabs(DDNSProvider): 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) @@ -1226,7 +1340,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: @@ -1250,15 +1364,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 = "http://dynupdate.noip.com/nic/update" def prepare_request_data(self, proto): assert proto == "ipv4" @@ -1271,6 +1385,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" @@ -1380,7 +1507,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. @@ -1398,11 +1525,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: @@ -1424,6 +1550,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 +1578,45 @@ 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 + + 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 +1625,7 @@ 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" @property def username(self): @@ -1572,11 +1744,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("