]>
git.ipfire.org Git - oddments/ddns.git/blob - src/ddns/providers.py
2 ###############################################################################
4 # ddns - A dynamic DNS client for IPFire #
5 # Copyright (C) 2012 IPFire development team #
7 # This program is free software: you can redistribute it and/or modify #
8 # it under the terms of the GNU General Public License as published by #
9 # the Free Software Foundation, either version 3 of the License, or #
10 # (at your option) any later version. #
12 # This program is distributed in the hope that it will be useful, #
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15 # GNU General Public License for more details. #
17 # You should have received a copy of the GNU General Public License #
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
20 ###############################################################################
27 import xml
.dom
.minidom
31 # Import all possible exception types.
34 logger
= logging
.getLogger("ddns.providers")
41 Returns a dict with all automatically registered providers.
43 return _providers
.copy()
45 class DDNSProvider(object):
46 # A short string that uniquely identifies
50 # The full name of the provider.
53 # A weburl to the homepage of the provider.
54 # (Where to register a new account?)
57 # A list of supported protocols.
58 protocols
= ("ipv6", "ipv4")
62 # holdoff time - Number of days no update is performed unless
63 # the IP address has changed.
66 # holdoff time for update failures - Number of days no update
67 # is tried after the last one has failed.
68 holdoff_failure_days
= 0.5
70 # True if the provider is able to remove records, too.
71 # Required to remove AAAA records if IPv6 is absent again.
72 can_remove_records
= True
74 # Automatically register all providers.
75 class __metaclass__(type):
76 def __init__(provider
, name
, bases
, dict):
77 type.__init
__(provider
, name
, bases
, dict)
79 # The main class from which is inherited is not registered
81 if name
== "DDNSProvider":
84 if not all((provider
.handle
, provider
.name
, provider
.website
)):
85 raise DDNSError(_("Provider is not properly configured"))
87 assert not _providers
.has_key(provider
.handle
), \
88 "Provider '%s' has already been registered" % provider
.handle
90 _providers
[provider
.handle
] = provider
95 Should be overwritten to check if the system the code is running
96 on has all the required tools to support this provider.
100 def __init__(self
, core
, **settings
):
103 # Copy a set of default settings and
104 # update them by those from the configuration file.
105 self
.settings
= self
.DEFAULT_SETTINGS
.copy()
106 self
.settings
.update(settings
)
109 return "<DDNS Provider %s (%s)>" % (self
.name
, self
.handle
)
111 def __cmp__(self
, other
):
112 return cmp(self
.hostname
, other
.hostname
)
118 def get(self
, key
, default
=None):
120 Get a setting from the settings dictionary.
122 return self
.settings
.get(key
, default
)
127 Fast access to the hostname.
129 return self
.get("hostname")
134 Fast access to the username.
136 return self
.get("username")
141 Fast access to the password.
143 return self
.get("password")
148 Fast access to the token.
150 return self
.get("token")
152 def __call__(self
, force
=False):
154 logger
.debug(_("Updating %s forced") % self
.hostname
)
156 # Do nothing if the last update has failed or no update is required
157 elif self
.has_failure
or not self
.requires_update
:
160 # Execute the update.
164 # In case of any errors, log the failed request and
165 # raise the exception.
166 except DDNSError
as e
:
167 self
.core
.db
.log_failure(self
.hostname
, e
)
170 logger
.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \
171 { "hostname" : self
.hostname
, "provider" : self
.name
})
172 self
.core
.db
.log_success(self
.hostname
)
175 for protocol
in self
.protocols
:
176 if self
.have_address(protocol
):
177 self
.update_protocol(protocol
)
178 elif self
.can_remove_records
:
179 self
.remove_protocol(protocol
)
181 def update_protocol(self
, proto
):
182 raise NotImplementedError
184 def remove_protocol(self
, proto
):
185 if not self
.can_remove_records
:
186 raise RuntimeError, "can_remove_records is enabled, but remove_protocol() not implemented"
188 raise NotImplementedError
191 def requires_update(self
):
192 # If the IP addresses have changed, an update is required
193 if self
.ip_address_changed(self
.protocols
):
194 logger
.debug(_("An update for %(hostname)s (%(provider)s)"
195 " is performed because of an IP address change") % \
196 { "hostname" : self
.hostname
, "provider" : self
.name
})
200 # If the holdoff time has expired, an update is required, too
201 if self
.holdoff_time_expired():
202 logger
.debug(_("An update for %(hostname)s (%(provider)s)"
203 " is performed because the holdoff time has expired") % \
204 { "hostname" : self
.hostname
, "provider" : self
.name
})
208 # Otherwise, we don't need to perform an update
209 logger
.debug(_("No update required for %(hostname)s (%(provider)s)") % \
210 { "hostname" : self
.hostname
, "provider" : self
.name
})
215 def has_failure(self
):
217 Returns True when the last update has failed and no retry
218 should be performed, yet.
220 last_status
= self
.db
.last_update_status(self
.hostname
)
222 # Return False if the last update has not failed.
223 if not last_status
== "failure":
226 # If there is no holdoff time, we won't update ever again.
227 if self
.holdoff_failure_days
is None:
228 logger
.warning(_("An update has not been performed because earlier updates failed for %s") \
230 logger
.warning(_("There will be no retries"))
234 # Determine when the holdoff time ends
235 last_update
= self
.db
.last_update(self
.hostname
, status
=last_status
)
236 holdoff_end
= last_update
+ datetime
.timedelta(days
=self
.holdoff_failure_days
)
238 now
= datetime
.datetime
.utcnow()
239 if now
< holdoff_end
:
240 failure_message
= self
.db
.last_update_failure_message(self
.hostname
)
242 logger
.warning(_("An update has not been performed because earlier updates failed for %s") \
246 logger
.warning(_("Last failure message:"))
248 for line
in failure_message
.splitlines():
249 logger
.warning(" %s" % line
)
251 logger
.warning(_("Further updates will be withheld until %s") % holdoff_end
)
257 def ip_address_changed(self
, protos
):
259 Returns True if this host is already up to date
260 and does not need to change the IP address on the
264 addresses
= self
.core
.system
.resolve(self
.hostname
, proto
)
265 current_address
= self
.get_address(proto
)
267 # Handle if the system has not got any IP address from a protocol
268 # (i.e. had full dual-stack connectivity which it has not any more)
269 if current_address
is None:
270 # If addresses still exists in the DNS system and if this provider
271 # is able to remove records, we will do that.
272 if addresses
and self
.can_remove_records
:
275 # Otherwise, we cannot go on...
278 if not current_address
in addresses
:
283 def holdoff_time_expired(self
):
285 Returns true if the holdoff time has expired
286 and the host requires an update
288 # If no holdoff days is defined, we cannot go on
289 if not self
.holdoff_days
:
292 # Get the timestamp of the last successfull update
293 last_update
= self
.db
.last_update(self
.hostname
, status
="success")
295 # If no timestamp has been recorded, no update has been
296 # performed. An update should be performed now.
300 # Determine when the holdoff time ends
301 holdoff_end
= last_update
+ datetime
.timedelta(days
=self
.holdoff_days
)
303 now
= datetime
.datetime
.utcnow()
305 if now
>= holdoff_end
:
306 logger
.debug("The holdoff time has expired for %s" % self
.hostname
)
309 logger
.debug("Updates for %s are held off until %s" % \
310 (self
.hostname
, holdoff_end
))
313 def send_request(self
, *args
, **kwargs
):
315 Proxy connection to the send request
318 return self
.core
.system
.send_request(*args
, **kwargs
)
320 def get_address(self
, proto
, default
=None):
322 Proxy method to get the current IP address.
324 return self
.core
.system
.get_address(proto
) or default
326 def have_address(self
, proto
):
328 Returns True if an IP address for the given protocol
331 address
= self
.get_address(proto
)
339 class DDNSProtocolDynDNS2(object):
341 This is an abstract class that implements the DynDNS updater
342 protocol version 2. As this is a popular way to update dynamic
343 DNS records, this class is supposed make the provider classes
347 # Information about the format of the request is to be found
348 # http://dyn.com/support/developers/api/perform-update/
349 # http://dyn.com/support/developers/api/return-codes/
351 # The DynDNS protocol version 2 does not allow to remove records
352 can_remove_records
= False
354 def prepare_request_data(self
, proto
):
356 "hostname" : self
.hostname
,
357 "myip" : self
.get_address(proto
),
362 def update_protocol(self
, proto
):
363 data
= self
.prepare_request_data(proto
)
365 return self
.send_request(data
)
367 def send_request(self
, data
):
368 # Send update to the server.
369 response
= DDNSProvider
.send_request(self
, self
.url
, data
=data
,
370 username
=self
.username
, password
=self
.password
)
372 # Get the full response message.
373 output
= response
.read()
375 # Handle success messages.
376 if output
.startswith("good") or output
.startswith("nochg"):
379 # Handle error codes.
380 if output
== "badauth":
381 raise DDNSAuthenticationError
382 elif output
== "abuse":
384 elif output
== "notfqdn":
385 raise DDNSRequestError(_("No valid FQDN was given."))
386 elif output
== "nohost":
387 raise DDNSRequestError(_("Specified host does not exist."))
388 elif output
== "911":
389 raise DDNSInternalServerError
390 elif output
== "dnserr":
391 raise DDNSInternalServerError(_("DNS error encountered."))
392 elif output
== "badagent":
393 raise DDNSBlockedError
395 # If we got here, some other update error happened.
396 raise DDNSUpdateError(_("Server response: %s") % output
)
399 class DDNSResponseParserXML(object):
401 This class provides a parser for XML responses which
402 will be sent by various providers. This class uses the python
403 shipped XML minidom module to walk through the XML tree and return
407 def get_xml_tag_value(self
, document
, content
):
408 # Send input to the parser.
409 xmldoc
= xml
.dom
.minidom
.parseString(document
)
411 # Get XML elements by the given content.
412 element
= xmldoc
.getElementsByTagName(content
)
414 # If no element has been found, we directly can return None.
418 # Only get the first child from an element, even there are more than one.
419 firstchild
= element
[0].firstChild
421 # Get the value of the child.
422 value
= firstchild
.nodeValue
428 class DDNSProviderAllInkl(DDNSProvider
):
429 handle
= "all-inkl.com"
430 name
= "All-inkl.com"
431 website
= "http://all-inkl.com/"
432 protocols
= ("ipv4",)
434 # There are only information provided by the vendor how to
435 # perform an update on a FRITZ Box. Grab requried informations
437 # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
439 url
= "http://dyndns.kasserver.com"
440 can_remove_records
= False
443 # There is no additional data required so we directly can
445 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
)
447 # Get the full response message.
448 output
= response
.read()
450 # Handle success messages.
451 if output
.startswith("good") or output
.startswith("nochg"):
454 # If we got here, some other update error happened.
455 raise DDNSUpdateError
458 class DDNSProviderBindNsupdate(DDNSProvider
):
460 name
= "BIND nsupdate utility"
461 website
= "http://en.wikipedia.org/wiki/Nsupdate"
467 # Search if the nsupdate utility is available
468 paths
= os
.environ
.get("PATH")
470 for path
in paths
.split(":"):
471 executable
= os
.path
.join(path
, "nsupdate")
473 if os
.path
.exists(executable
):
479 scriptlet
= self
.__make
_scriptlet
()
481 # -v enables TCP hence we transfer keys and other data that may
482 # exceed the size of one packet.
483 # -t sets the timeout
484 command
= ["nsupdate", "-v", "-t", "60"]
486 p
= subprocess
.Popen(command
, shell
=True,
487 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
489 stdout
, stderr
= p
.communicate(scriptlet
)
491 if p
.returncode
== 0:
494 raise DDNSError("nsupdate terminated with error code: %s\n %s" % (p
.returncode
, stderr
))
496 def __make_scriptlet(self
):
499 # Set a different server the update is sent to.
500 server
= self
.get("server", None)
502 scriptlet
.append("server %s" % server
)
504 # Set the DNS zone the host should be added to.
505 zone
= self
.get("zone", None)
507 scriptlet
.append("zone %s" % zone
)
509 key
= self
.get("key", None)
511 secret
= self
.get("secret")
513 scriptlet
.append("key %s %s" % (key
, secret
))
515 ttl
= self
.get("ttl", self
.DEFAULT_TTL
)
517 # Perform an update for each supported protocol.
518 for rrtype
, proto
in (("AAAA", "ipv6"), ("A", "ipv4")):
519 address
= self
.get_address(proto
)
523 scriptlet
.append("update delete %s. %s" % (self
.hostname
, rrtype
))
524 scriptlet
.append("update add %s. %s %s %s" % \
525 (self
.hostname
, ttl
, rrtype
, address
))
527 # Send the actions to the server.
528 scriptlet
.append("send")
529 scriptlet
.append("quit")
531 logger
.debug(_("Scriptlet:"))
532 for line
in scriptlet
:
533 # Masquerade the line with the secret key.
534 if line
.startswith("key"):
535 line
= "key **** ****"
537 logger
.debug(" %s" % line
)
539 return "\n".join(scriptlet
)
542 class DDNSProviderChangeIP(DDNSProvider
):
543 handle
= "changeip.com"
544 name
= "ChangeIP.com"
545 website
= "https://changeip.com"
546 protocols
= ("ipv4",)
548 # Detailed information about the update api can be found here.
549 # http://www.changeip.com/accounts/knowledgebase.php?action=displayarticle&id=34
551 url
= "https://nic.changeip.com/nic/update"
552 can_remove_records
= False
554 def update_protocol(self
, proto
):
556 "hostname" : self
.hostname
,
557 "myip" : self
.get_address(proto
),
560 # Send update to the server.
562 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
565 # Handle error codes.
566 except urllib2
.HTTPError
, e
:
568 raise DDNSRequestError(_("Domain not found."))
572 # Handle success message.
573 if response
.code
== 200:
576 # If we got here, some other update error happened.
577 raise DDNSUpdateError(_("Server response: %s") % output
)
580 class DDNSProviderDDNSS(DDNSProvider
):
583 website
= "http://www.ddnss.de"
584 protocols
= ("ipv4",)
586 # Detailed information about how to send the update request and possible response
587 # codes can be obtained from here.
588 # http://www.ddnss.de/info.php
589 # http://www.megacomputing.de/2014/08/dyndns-service-response-time/#more-919
591 url
= "http://www.ddnss.de/upd.php"
592 can_remove_records
= False
594 def update_protocol(self
, proto
):
596 "ip" : self
.get_address(proto
),
597 "host" : self
.hostname
,
600 # Check if a token has been set.
602 data
["key"] = self
.token
604 # Check if username and hostname are given.
605 elif self
.username
and self
.password
:
607 "user" : self
.username
,
608 "pwd" : self
.password
,
611 # Raise an error if no auth details are given.
613 raise DDNSConfigurationError
615 # Send update to the server.
616 response
= self
.send_request(self
.url
, data
=data
)
618 # This provider sends the response code as part of the header.
619 header
= response
.info()
621 # Get status information from the header.
622 output
= header
.getheader('ddnss-response')
624 # Handle success messages.
625 if output
== "good" or output
== "nochg":
628 # Handle error codes.
629 if output
== "badauth":
630 raise DDNSAuthenticationError
631 elif output
== "notfqdn":
632 raise DDNSRequestError(_("No valid FQDN was given."))
633 elif output
== "nohost":
634 raise DDNSRequestError(_("Specified host does not exist."))
635 elif output
== "911":
636 raise DDNSInternalServerError
637 elif output
== "dnserr":
638 raise DDNSInternalServerError(_("DNS error encountered."))
639 elif output
== "disabled":
640 raise DDNSRequestError(_("Account disabled or locked."))
642 # If we got here, some other update error happened.
643 raise DDNSUpdateError
646 class DDNSProviderDHS(DDNSProvider
):
648 name
= "DHS International"
649 website
= "http://dhs.org/"
650 protocols
= ("ipv4",)
652 # No information about the used update api provided on webpage,
653 # grabed from source code of ez-ipudate.
655 url
= "http://members.dhs.org/nic/hosts"
656 can_remove_records
= False
658 def update_protocol(self
, proto
):
660 "domain" : self
.hostname
,
661 "ip" : self
.get_address(proto
),
663 "hostcmdstage" : "2",
667 # Send update to the server.
668 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
671 # Handle success messages.
672 if response
.code
== 200:
675 # If we got here, some other update error happened.
676 raise DDNSUpdateError
679 class DDNSProviderDNSpark(DDNSProvider
):
680 handle
= "dnspark.com"
682 website
= "http://dnspark.com/"
683 protocols
= ("ipv4",)
685 # Informations to the used api can be found here:
686 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
688 url
= "https://control.dnspark.com/api/dynamic/update.php"
689 can_remove_records
= False
691 def update_protocol(self
, proto
):
693 "domain" : self
.hostname
,
694 "ip" : self
.get_address(proto
),
697 # Send update to the server.
698 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
701 # Get the full response message.
702 output
= response
.read()
704 # Handle success messages.
705 if output
.startswith("ok") or output
.startswith("nochange"):
708 # Handle error codes.
709 if output
== "unauth":
710 raise DDNSAuthenticationError
711 elif output
== "abuse":
713 elif output
== "blocked":
714 raise DDNSBlockedError
715 elif output
== "nofqdn":
716 raise DDNSRequestError(_("No valid FQDN was given."))
717 elif output
== "nohost":
718 raise DDNSRequestError(_("Invalid hostname specified."))
719 elif output
== "notdyn":
720 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
721 elif output
== "invalid":
722 raise DDNSRequestError(_("Invalid IP address has been sent."))
724 # If we got here, some other update error happened.
725 raise DDNSUpdateError
728 class DDNSProviderDtDNS(DDNSProvider
):
731 website
= "http://dtdns.com/"
732 protocols
= ("ipv4",)
734 # Information about the format of the HTTPS request is to be found
735 # http://www.dtdns.com/dtsite/updatespec
737 url
= "https://www.dtdns.com/api/autodns.cfm"
738 can_remove_records
= False
740 def update_protocol(self
, proto
):
742 "ip" : self
.get_address(proto
),
743 "id" : self
.hostname
,
747 # Send update to the server.
748 response
= self
.send_request(self
.url
, data
=data
)
750 # Get the full response message.
751 output
= response
.read()
753 # Remove all leading and trailing whitespace.
754 output
= output
.strip()
756 # Handle success messages.
757 if "now points to" in output
:
760 # Handle error codes.
761 if output
== "No hostname to update was supplied.":
762 raise DDNSRequestError(_("No hostname specified."))
764 elif output
== "The hostname you supplied is not valid.":
765 raise DDNSRequestError(_("Invalid hostname specified."))
767 elif output
== "The password you supplied is not valid.":
768 raise DDNSAuthenticationError
770 elif output
== "Administration has disabled this account.":
771 raise DDNSRequestError(_("Account has been disabled."))
773 elif output
== "Illegal character in IP.":
774 raise DDNSRequestError(_("Invalid IP address has been sent."))
776 elif output
== "Too many failed requests.":
777 raise DDNSRequestError(_("Too many failed requests."))
779 # If we got here, some other update error happened.
780 raise DDNSUpdateError
783 class DDNSProviderDynDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
784 handle
= "dyndns.org"
786 website
= "http://dyn.com/dns/"
787 protocols
= ("ipv4",)
789 # Information about the format of the request is to be found
790 # http://http://dyn.com/support/developers/api/perform-update/
791 # http://dyn.com/support/developers/api/return-codes/
793 url
= "https://members.dyndns.org/nic/update"
796 class DDNSProviderDynU(DDNSProtocolDynDNS2
, DDNSProvider
):
799 website
= "http://dynu.com/"
800 protocols
= ("ipv6", "ipv4",)
802 # Detailed information about the request and response codes
803 # are available on the providers webpage.
804 # http://dynu.com/Default.aspx?page=dnsapi
806 url
= "https://api.dynu.com/nic/update"
808 # DynU sends the IPv6 and IPv4 address in one request
811 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, "ipv4")
813 # This one supports IPv6
814 myipv6
= self
.get_address("ipv6")
816 # Add update information if we have an IPv6 address.
818 data
["myipv6"] = myipv6
820 self
.send_request(data
)
823 class DDNSProviderEasyDNS(DDNSProvider
):
824 handle
= "easydns.com"
826 website
= "http://www.easydns.com/"
827 protocols
= ("ipv4",)
829 # Detailed information about the request and response codes
830 # (API 1.3) are available on the providers webpage.
831 # https://fusion.easydns.com/index.php?/Knowledgebase/Article/View/102/7/dynamic-dns
833 url
= "http://api.cp.easydns.com/dyn/tomato.php"
835 def update_protocol(self
, proto
):
837 "myip" : self
.get_address(proto
, "-"),
838 "hostname" : self
.hostname
,
841 # Send update to the server.
842 response
= self
.send_request(self
.url
, data
=data
,
843 username
=self
.username
, password
=self
.password
)
845 # Get the full response message.
846 output
= response
.read()
848 # Remove all leading and trailing whitespace.
849 output
= output
.strip()
851 # Handle success messages.
852 if output
.startswith("NOERROR"):
855 # Handle error codes.
856 if output
.startswith("NOACCESS"):
857 raise DDNSAuthenticationError
859 elif output
.startswith("NOSERVICE"):
860 raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain."))
862 elif output
.startswith("ILLEGAL INPUT"):
863 raise DDNSRequestError(_("Invalid data has been sent."))
865 elif output
.startswith("TOOSOON"):
866 raise DDNSRequestError(_("Too frequent update requests have been sent."))
868 # If we got here, some other update error happened.
869 raise DDNSUpdateError
872 class DDNSProviderDomopoli(DDNSProtocolDynDNS2
, DDNSProvider
):
873 handle
= "domopoli.de"
875 website
= "http://domopoli.de/"
876 protocols
= ("ipv4",)
878 # https://www.domopoli.de/?page=howto#DynDns_start
880 url
= "http://dyndns.domopoli.de/nic/update"
883 class DDNSProviderDynsNet(DDNSProvider
):
886 website
= "http://www.dyns.net/"
887 protocols
= ("ipv4",)
888 can_remove_records
= False
890 # There is very detailed informatio about how to send the update request and
891 # the possible response codes. (Currently we are using the v1.1 proto)
892 # http://www.dyns.net/documentation/technical/protocol/
894 url
= "http://www.dyns.net/postscript011.php"
896 def update_protocol(self
, proto
):
898 "ip" : self
.get_address(proto
),
899 "host" : self
.hostname
,
900 "username" : self
.username
,
901 "password" : self
.password
,
904 # Send update to the server.
905 response
= self
.send_request(self
.url
, data
=data
)
907 # Get the full response message.
908 output
= response
.read()
910 # Handle success messages.
911 if output
.startswith("200"):
914 # Handle error codes.
915 if output
.startswith("400"):
916 raise DDNSRequestError(_("Malformed request has been sent."))
917 elif output
.startswith("401"):
918 raise DDNSAuthenticationError
919 elif output
.startswith("402"):
920 raise DDNSRequestError(_("Too frequent update requests have been sent."))
921 elif output
.startswith("403"):
922 raise DDNSInternalServerError
924 # If we got here, some other update error happened.
925 raise DDNSUpdateError(_("Server response: %s") % output
)
928 class DDNSProviderEnomCom(DDNSResponseParserXML
, DDNSProvider
):
931 website
= "http://www.enom.com/"
932 protocols
= ("ipv4",)
934 # There are very detailed information about how to send an update request and
936 # http://www.enom.com/APICommandCatalog/
938 url
= "https://dynamic.name-services.com/interface.asp"
939 can_remove_records
= False
941 def update_protocol(self
, proto
):
943 "command" : "setdnshost",
944 "responsetype" : "xml",
945 "address" : self
.get_address(proto
),
946 "domainpassword" : self
.password
,
947 "zone" : self
.hostname
950 # Send update to the server.
951 response
= self
.send_request(self
.url
, data
=data
)
953 # Get the full response message.
954 output
= response
.read()
956 # Handle success messages.
957 if self
.get_xml_tag_value(output
, "ErrCount") == "0":
960 # Handle error codes.
961 errorcode
= self
.get_xml_tag_value(output
, "ResponseNumber")
963 if errorcode
== "304155":
964 raise DDNSAuthenticationError
965 elif errorcode
== "304153":
966 raise DDNSRequestError(_("Domain not found."))
968 # If we got here, some other update error happened.
969 raise DDNSUpdateError
972 class DDNSProviderEntryDNS(DDNSProvider
):
973 handle
= "entrydns.net"
975 website
= "http://entrydns.net/"
976 protocols
= ("ipv4",)
978 # Some very tiny details about their so called "Simple API" can be found
979 # here: https://entrydns.net/help
980 url
= "https://entrydns.net/records/modify"
981 can_remove_records
= False
983 def update_protocol(self
, proto
):
985 "ip" : self
.get_address(proto
),
988 # Add auth token to the update url.
989 url
= "%s/%s" % (self
.url
, self
.token
)
991 # Send update to the server.
993 response
= self
.send_request(url
, data
=data
)
996 except urllib2
.HTTPError
, e
:
998 raise DDNSAuthenticationError
1001 raise DDNSRequestError(_("An invalid IP address was submitted"))
1005 # Handle success messages.
1006 if response
.code
== 200:
1009 # If we got here, some other update error happened.
1010 raise DDNSUpdateError
1013 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider
):
1014 handle
= "freedns.afraid.org"
1015 name
= "freedns.afraid.org"
1016 website
= "http://freedns.afraid.org/"
1018 # No information about the request or response could be found on the vendor
1019 # page. All used values have been collected by testing.
1020 url
= "https://freedns.afraid.org/dynamic/update.php"
1021 can_remove_records
= False
1023 def update_protocol(self
, proto
):
1025 "address" : self
.get_address(proto
),
1028 # Add auth token to the update url.
1029 url
= "%s?%s" % (self
.url
, self
.token
)
1031 # Send update to the server.
1032 response
= self
.send_request(url
, data
=data
)
1034 # Get the full response message.
1035 output
= response
.read()
1037 # Handle success messages.
1038 if output
.startswith("Updated") or "has not changed" in output
:
1041 # Handle error codes.
1042 if output
== "ERROR: Unable to locate this record":
1043 raise DDNSAuthenticationError
1044 elif "is an invalid IP address" in output
:
1045 raise DDNSRequestError(_("Invalid IP address has been sent."))
1047 # If we got here, some other update error happened.
1048 raise DDNSUpdateError
1051 class DDNSProviderJoker(DDNSProtocolDynDNS2
, DDNSProvider
):
1052 handle
= "joker.com"
1053 name
= "Joker.com Dynamic DNS"
1054 website
= "https://joker.com/"
1055 protocols
= ("ipv4",)
1057 # Information about the request can be found here:
1058 # https://joker.com/faq/content/11/427/en/what-is-dynamic-dns-dyndns.html
1059 # Using DynDNS V2 protocol over HTTPS here
1061 url
= "https://svc.joker.com/nic/update"
1064 class DDNSProviderGoogle(DDNSProtocolDynDNS2
, DDNSProvider
):
1065 handle
= "domains.google.com"
1066 name
= "Google Domains"
1067 website
= "https://domains.google.com/"
1068 protocols
= ("ipv4",)
1070 # Information about the format of the HTTP request is to be found
1071 # here: https://support.google.com/domains/answer/6147083?hl=en
1073 url
= "https://domains.google.com/nic/update"
1076 class DDNSProviderLightningWireLabs(DDNSProvider
):
1077 handle
= "dns.lightningwirelabs.com"
1078 name
= "Lightning Wire Labs DNS Service"
1079 website
= "http://dns.lightningwirelabs.com/"
1081 # Information about the format of the HTTPS request is to be found
1082 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
1084 url
= "https://dns.lightningwirelabs.com/update"
1088 "hostname" : self
.hostname
,
1089 "address6" : self
.get_address("ipv6", "-"),
1090 "address4" : self
.get_address("ipv4", "-"),
1093 # Check if a token has been set.
1095 data
["token"] = self
.token
1097 # Check for username and password.
1098 elif self
.username
and self
.password
:
1100 "username" : self
.username
,
1101 "password" : self
.password
,
1104 # Raise an error if no auth details are given.
1106 raise DDNSConfigurationError
1108 # Send update to the server.
1109 response
= self
.send_request(self
.url
, data
=data
)
1111 # Handle success messages.
1112 if response
.code
== 200:
1115 # If we got here, some other update error happened.
1116 raise DDNSUpdateError
1119 class DDNSProviderLoopia(DDNSProtocolDynDNS2
, DDNSProvider
):
1120 handle
= "loopia.se"
1122 website
= "https://www.loopia.com"
1123 protocols
= ("ipv4",)
1125 # Information about the format of the HTTP request is to be found
1126 # here: https://support.loopia.com/wiki/About_the_DynDNS_support
1128 url
= "https://dns.loopia.se/XDynDNSServer/XDynDNS.php"
1131 class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2
, DDNSProvider
):
1132 handle
= "myonlineportal.net"
1133 name
= "myonlineportal.net"
1134 website
= "https:/myonlineportal.net/"
1136 # Information about the request and response can be obtained here:
1137 # https://myonlineportal.net/howto_dyndns
1139 url
= "https://myonlineportal.net/updateddns"
1141 def prepare_request_data(self
, proto
):
1143 "hostname" : self
.hostname
,
1144 "ip" : self
.get_address(proto
),
1150 class DDNSProviderNamecheap(DDNSResponseParserXML
, DDNSProvider
):
1151 handle
= "namecheap.com"
1153 website
= "http://namecheap.com"
1154 protocols
= ("ipv4",)
1156 # Information about the format of the HTTP request is to be found
1157 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
1158 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
1160 url
= "https://dynamicdns.park-your-domain.com/update"
1161 can_remove_records
= False
1163 def update_protocol(self
, proto
):
1164 # Namecheap requires the hostname splitted into a host and domain part.
1165 host
, domain
= self
.hostname
.split(".", 1)
1168 "ip" : self
.get_address(proto
),
1169 "password" : self
.password
,
1174 # Send update to the server.
1175 response
= self
.send_request(self
.url
, data
=data
)
1177 # Get the full response message.
1178 output
= response
.read()
1180 # Handle success messages.
1181 if self
.get_xml_tag_value(output
, "IP") == address
:
1184 # Handle error codes.
1185 errorcode
= self
.get_xml_tag_value(output
, "ResponseNumber")
1187 if errorcode
== "304156":
1188 raise DDNSAuthenticationError
1189 elif errorcode
== "316153":
1190 raise DDNSRequestError(_("Domain not found."))
1191 elif errorcode
== "316154":
1192 raise DDNSRequestError(_("Domain not active."))
1193 elif errorcode
in ("380098", "380099"):
1194 raise DDNSInternalServerError
1196 # If we got here, some other update error happened.
1197 raise DDNSUpdateError
1200 class DDNSProviderNOIP(DDNSProtocolDynDNS2
, DDNSProvider
):
1201 handle
= "no-ip.com"
1203 website
= "http://www.no-ip.com/"
1204 protocols
= ("ipv4",)
1206 # Information about the format of the HTTP request is to be found
1207 # here: http://www.no-ip.com/integrate/request and
1208 # here: http://www.no-ip.com/integrate/response
1210 url
= "http://dynupdate.no-ip.com/nic/update"
1212 def prepare_request_data(self
, proto
):
1213 assert proto
== "ipv4"
1216 "hostname" : self
.hostname
,
1217 "address" : self
.get_address(proto
),
1223 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2
, DDNSProvider
):
1224 handle
= "nsupdate.info"
1225 name
= "nsupdate.info"
1226 website
= "http://nsupdate.info/"
1227 protocols
= ("ipv6", "ipv4",)
1229 # Information about the format of the HTTP request can be found
1230 # after login on the provider user interface and here:
1231 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1233 url
= "https://nsupdate.info/nic/update"
1235 # TODO nsupdate.info can actually do this, but the functionality
1236 # has not been implemented here, yet.
1237 can_remove_records
= False
1239 # After a failed update, there will be no retries
1240 # https://bugzilla.ipfire.org/show_bug.cgi?id=10603
1241 holdoff_failure_days
= None
1243 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1244 # and for the password a so called secret.
1247 return self
.get("hostname")
1251 return self
.token
or self
.get("secret")
1253 def prepare_request_data(self
, proto
):
1255 "myip" : self
.get_address(proto
),
1261 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
1262 handle
= "opendns.com"
1264 website
= "http://www.opendns.com"
1266 # Detailed information about the update request and possible
1267 # response codes can be obtained from here:
1268 # https://support.opendns.com/entries/23891440
1270 url
= "https://updates.opendns.com/nic/update"
1272 def prepare_request_data(self
, proto
):
1274 "hostname" : self
.hostname
,
1275 "myip" : self
.get_address(proto
),
1281 class DDNSProviderOVH(DDNSProtocolDynDNS2
, DDNSProvider
):
1284 website
= "http://www.ovh.com/"
1285 protocols
= ("ipv4",)
1287 # OVH only provides very limited information about how to
1288 # update a DynDNS host. They only provide the update url
1289 # on the their german subpage.
1291 # http://hilfe.ovh.de/DomainDynHost
1293 url
= "https://www.ovh.com/nic/update"
1295 def prepare_request_data(self
, proto
):
1296 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, proto
)
1298 "system" : "dyndns",
1304 class DDNSProviderRegfish(DDNSProvider
):
1305 handle
= "regfish.com"
1306 name
= "Regfish GmbH"
1307 website
= "http://www.regfish.com/"
1309 # A full documentation to the providers api can be found here
1310 # but is only available in german.
1311 # https://www.regfish.de/domains/dyndns/dokumentation
1313 url
= "https://dyndns.regfish.de/"
1314 can_remove_records
= False
1318 "fqdn" : self
.hostname
,
1321 # Check if we update an IPv6 address.
1322 address6
= self
.get_address("ipv6")
1324 data
["ipv6"] = address6
1326 # Check if we update an IPv4 address.
1327 address4
= self
.get_address("ipv4")
1329 data
["ipv4"] = address4
1331 # Raise an error if none address is given.
1332 if not data
.has_key("ipv6") and not data
.has_key("ipv4"):
1333 raise DDNSConfigurationError
1335 # Check if a token has been set.
1337 data
["token"] = self
.token
1339 # Raise an error if no token and no useranem and password
1341 elif not self
.username
and not self
.password
:
1342 raise DDNSConfigurationError(_("No Auth details specified."))
1344 # HTTP Basic Auth is only allowed if no token is used.
1346 # Send update to the server.
1347 response
= self
.send_request(self
.url
, data
=data
)
1349 # Send update to the server.
1350 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
1353 # Get the full response message.
1354 output
= response
.read()
1356 # Handle success messages.
1357 if "100" in output
or "101" in output
:
1360 # Handle error codes.
1361 if "401" or "402" in output
:
1362 raise DDNSAuthenticationError
1363 elif "408" in output
:
1364 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1365 elif "409" in output
:
1366 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1367 elif "412" in output
:
1368 raise DDNSRequestError(_("No valid FQDN was given."))
1369 elif "414" in output
:
1370 raise DDNSInternalServerError
1372 # If we got here, some other update error happened.
1373 raise DDNSUpdateError
1376 class DDNSProviderSelfhost(DDNSProtocolDynDNS2
, DDNSProvider
):
1377 handle
= "selfhost.de"
1378 name
= "Selfhost.de"
1379 website
= "http://www.selfhost.de/"
1380 protocols
= ("ipv4",)
1382 url
= "https://carol.selfhost.de/nic/update"
1384 def prepare_request_data(self
, proto
):
1385 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, proto
)
1393 class DDNSProviderSPDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
1394 handle
= "spdns.org"
1396 website
= "http://spdns.org/"
1398 # Detailed information about request and response codes are provided
1399 # by the vendor. They are using almost the same mechanism and status
1400 # codes as dyndns.org so we can inherit all those stuff.
1402 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1403 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1405 url
= "https://update.spdns.de/nic/update"
1409 return self
.get("username") or self
.hostname
1413 return self
.get("password") or self
.token
1416 class DDNSProviderStrato(DDNSProtocolDynDNS2
, DDNSProvider
):
1417 handle
= "strato.com"
1419 website
= "http:/www.strato.com/"
1420 protocols
= ("ipv4",)
1422 # Information about the request and response can be obtained here:
1423 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1425 url
= "https://dyndns.strato.com/nic/update"
1427 def prepare_request_data(self
, proto
):
1428 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, proto
)
1431 "backupmx" : "NOCHG"
1437 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
1438 handle
= "twodns.de"
1440 website
= "http://www.twodns.de"
1441 protocols
= ("ipv4",)
1443 # Detailed information about the request can be found here
1444 # http://twodns.de/en/faqs
1445 # http://twodns.de/en/api
1447 url
= "https://update.twodns.de/update"
1449 def prepare_request_data(self
, proto
):
1450 assert proto
== "ipv4"
1453 "ip" : self
.get_address(proto
),
1454 "hostname" : self
.hostname
1460 class DDNSProviderUdmedia(DDNSProtocolDynDNS2
, DDNSProvider
):
1461 handle
= "udmedia.de"
1462 name
= "Udmedia GmbH"
1463 website
= "http://www.udmedia.de"
1464 protocols
= ("ipv4",)
1466 # Information about the request can be found here
1467 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1469 url
= "https://www.udmedia.de/nic/update"
1472 class DDNSProviderVariomedia(DDNSProtocolDynDNS2
, DDNSProvider
):
1473 handle
= "variomedia.de"
1475 website
= "http://www.variomedia.de/"
1476 protocols
= ("ipv6", "ipv4",)
1478 # Detailed information about the request can be found here
1479 # https://dyndns.variomedia.de/
1481 url
= "https://dyndns.variomedia.de/nic/update"
1483 def prepare_request_data(self
, proto
):
1485 "hostname" : self
.hostname
,
1486 "myip" : self
.get_address(proto
),
1492 class DDNSProviderXLhost(DDNSProtocolDynDNS2
, DDNSProvider
):
1493 handle
= "xlhost.de"
1495 website
= "http://xlhost.de/"
1496 protocols
= ("ipv4",)
1498 # Information about the format of the HTTP request is to be found
1499 # here: https://xlhost.de/faq/index_html?topicId=CQA2ELIPO4SQ
1501 url
= "https://nsupdate.xlhost.de/"
1504 class DDNSProviderZoneedit(DDNSProvider
):
1505 handle
= "zoneedit.com"
1507 website
= "http://www.zoneedit.com"
1508 protocols
= ("ipv4",)
1510 # Detailed information about the request and the response codes can be
1512 # http://www.zoneedit.com/doc/api/other.html
1513 # http://www.zoneedit.com/faq.html
1515 url
= "https://dynamic.zoneedit.com/auth/dynamic.html"
1517 def update_protocol(self
, proto
):
1519 "dnsto" : self
.get_address(proto
),
1520 "host" : self
.hostname
1523 # Send update to the server.
1524 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
1527 # Get the full response message.
1528 output
= response
.read()
1530 # Handle success messages.
1531 if output
.startswith("<SUCCESS"):
1534 # Handle error codes.
1535 if output
.startswith("invalid login"):
1536 raise DDNSAuthenticationError
1537 elif output
.startswith("<ERROR CODE=\"704\""):
1538 raise DDNSRequestError(_("No valid FQDN was given."))
1539 elif output
.startswith("<ERROR CODE=\"702\""):
1540 raise DDNSInternalServerError
1542 # If we got here, some other update error happened.
1543 raise DDNSUpdateError
1546 class DDNSProviderZZZZ(DDNSProvider
):
1549 website
= "https://zzzz.io"
1550 protocols
= ("ipv6", "ipv4",)
1552 # Detailed information about the update request can be found here:
1553 # https://zzzz.io/faq/
1555 # Details about the possible response codes have been provided in the bugtracker:
1556 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1558 url
= "https://zzzz.io/api/v1/update"
1559 can_remove_records
= False
1561 def update_protocol(self
, proto
):
1563 "ip" : self
.get_address(proto
),
1564 "token" : self
.token
,
1568 data
["type"] = "aaaa"
1570 # zzzz uses the host from the full hostname as part
1571 # of the update url.
1572 host
, domain
= self
.hostname
.split(".", 1)
1574 # Add host value to the update url.
1575 url
= "%s/%s" % (self
.url
, host
)
1577 # Send update to the server.
1579 response
= self
.send_request(url
, data
=data
)
1581 # Handle error codes.
1582 except DDNSNotFound
:
1583 raise DDNSRequestError(_("Invalid hostname specified"))
1585 # Handle success messages.
1586 if response
.code
== 200:
1589 # If we got here, some other update error happened.
1590 raise DDNSUpdateError