]>
git.ipfire.org Git - ddns.git/blob - src/ddns/providers.py
6ac556444553fbf0d6e8b23854fe228ad6c81fc5
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 # 1) Catch network errors early, because we do not want to log
165 # them to the database. They are usually temporary and caused
166 # by the client side, so that we will retry quickly.
167 # 2) If there is an internet server error (HTTP code 500) on the
168 # provider's site, we will not log a failure and try again
170 except (DDNSNetworkError
, DDNSInternalServerError
):
173 # In case of any errors, log the failed request and
174 # raise the exception.
175 except DDNSError
as e
:
176 self
.core
.db
.log_failure(self
.hostname
, e
)
179 logger
.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \
180 { "hostname" : self
.hostname
, "provider" : self
.name
})
181 self
.core
.db
.log_success(self
.hostname
)
184 for protocol
in self
.protocols
:
185 if self
.have_address(protocol
):
186 self
.update_protocol(protocol
)
187 elif self
.can_remove_records
:
188 self
.remove_protocol(protocol
)
190 def update_protocol(self
, proto
):
191 raise NotImplementedError
193 def remove_protocol(self
, proto
):
194 if not self
.can_remove_records
:
195 raise RuntimeError, "can_remove_records is enabled, but remove_protocol() not implemented"
197 raise NotImplementedError
200 def requires_update(self
):
201 # If the IP addresses have changed, an update is required
202 if self
.ip_address_changed(self
.protocols
):
203 logger
.debug(_("An update for %(hostname)s (%(provider)s)"
204 " is performed because of an IP address change") % \
205 { "hostname" : self
.hostname
, "provider" : self
.name
})
209 # If the holdoff time has expired, an update is required, too
210 if self
.holdoff_time_expired():
211 logger
.debug(_("An update for %(hostname)s (%(provider)s)"
212 " is performed because the holdoff time has expired") % \
213 { "hostname" : self
.hostname
, "provider" : self
.name
})
217 # Otherwise, we don't need to perform an update
218 logger
.debug(_("No update required for %(hostname)s (%(provider)s)") % \
219 { "hostname" : self
.hostname
, "provider" : self
.name
})
224 def has_failure(self
):
226 Returns True when the last update has failed and no retry
227 should be performed, yet.
229 last_status
= self
.db
.last_update_status(self
.hostname
)
231 # Return False if the last update has not failed.
232 if not last_status
== "failure":
235 # If there is no holdoff time, we won't update ever again.
236 if self
.holdoff_failure_days
is None:
237 logger
.warning(_("An update has not been performed because earlier updates failed for %s") \
239 logger
.warning(_("There will be no retries"))
243 # Determine when the holdoff time ends
244 last_update
= self
.db
.last_update(self
.hostname
, status
=last_status
)
245 holdoff_end
= last_update
+ datetime
.timedelta(days
=self
.holdoff_failure_days
)
247 now
= datetime
.datetime
.utcnow()
248 if now
< holdoff_end
:
249 failure_message
= self
.db
.last_update_failure_message(self
.hostname
)
251 logger
.warning(_("An update has not been performed because earlier updates failed for %s") \
255 logger
.warning(_("Last failure message:"))
257 for line
in failure_message
.splitlines():
258 logger
.warning(" %s" % line
)
260 logger
.warning(_("Further updates will be withheld until %s") % holdoff_end
)
266 def ip_address_changed(self
, protos
):
268 Returns True if this host is already up to date
269 and does not need to change the IP address on the
273 addresses
= self
.core
.system
.resolve(self
.hostname
, proto
)
274 current_address
= self
.get_address(proto
)
276 # Handle if the system has not got any IP address from a protocol
277 # (i.e. had full dual-stack connectivity which it has not any more)
278 if current_address
is None:
279 # If addresses still exists in the DNS system and if this provider
280 # is able to remove records, we will do that.
281 if addresses
and self
.can_remove_records
:
284 # Otherwise, we cannot go on...
287 if not current_address
in addresses
:
292 def holdoff_time_expired(self
):
294 Returns true if the holdoff time has expired
295 and the host requires an update
297 # If no holdoff days is defined, we cannot go on
298 if not self
.holdoff_days
:
301 # Get the timestamp of the last successfull update
302 last_update
= self
.db
.last_update(self
.hostname
, status
="success")
304 # If no timestamp has been recorded, no update has been
305 # performed. An update should be performed now.
309 # Determine when the holdoff time ends
310 holdoff_end
= last_update
+ datetime
.timedelta(days
=self
.holdoff_days
)
312 now
= datetime
.datetime
.utcnow()
314 if now
>= holdoff_end
:
315 logger
.debug("The holdoff time has expired for %s" % self
.hostname
)
318 logger
.debug("Updates for %s are held off until %s" % \
319 (self
.hostname
, holdoff_end
))
322 def send_request(self
, *args
, **kwargs
):
324 Proxy connection to the send request
327 return self
.core
.system
.send_request(*args
, **kwargs
)
329 def get_address(self
, proto
, default
=None):
331 Proxy method to get the current IP address.
333 return self
.core
.system
.get_address(proto
) or default
335 def have_address(self
, proto
):
337 Returns True if an IP address for the given protocol
340 address
= self
.get_address(proto
)
348 class DDNSProtocolDynDNS2(object):
350 This is an abstract class that implements the DynDNS updater
351 protocol version 2. As this is a popular way to update dynamic
352 DNS records, this class is supposed make the provider classes
356 # Information about the format of the request is to be found
357 # http://dyn.com/support/developers/api/perform-update/
358 # http://dyn.com/support/developers/api/return-codes/
360 # The DynDNS protocol version 2 does not allow to remove records
361 can_remove_records
= False
363 def prepare_request_data(self
, proto
):
365 "hostname" : self
.hostname
,
366 "myip" : self
.get_address(proto
),
371 def update_protocol(self
, proto
):
372 data
= self
.prepare_request_data(proto
)
374 return self
.send_request(data
)
376 def send_request(self
, data
):
377 # Send update to the server.
378 response
= DDNSProvider
.send_request(self
, self
.url
, data
=data
,
379 username
=self
.username
, password
=self
.password
)
381 # Get the full response message.
382 output
= response
.read()
384 # Handle success messages.
385 if output
.startswith("good") or output
.startswith("nochg"):
388 # Handle error codes.
389 if output
== "badauth":
390 raise DDNSAuthenticationError
391 elif output
== "abuse":
393 elif output
== "notfqdn":
394 raise DDNSRequestError(_("No valid FQDN was given."))
395 elif output
== "nohost":
396 raise DDNSRequestError(_("Specified host does not exist."))
397 elif output
== "911":
398 raise DDNSInternalServerError
399 elif output
== "dnserr":
400 raise DDNSInternalServerError(_("DNS error encountered."))
401 elif output
== "badagent":
402 raise DDNSBlockedError
404 # If we got here, some other update error happened.
405 raise DDNSUpdateError(_("Server response: %s") % output
)
408 class DDNSResponseParserXML(object):
410 This class provides a parser for XML responses which
411 will be sent by various providers. This class uses the python
412 shipped XML minidom module to walk through the XML tree and return
416 def get_xml_tag_value(self
, document
, content
):
417 # Send input to the parser.
418 xmldoc
= xml
.dom
.minidom
.parseString(document
)
420 # Get XML elements by the given content.
421 element
= xmldoc
.getElementsByTagName(content
)
423 # If no element has been found, we directly can return None.
427 # Only get the first child from an element, even there are more than one.
428 firstchild
= element
[0].firstChild
430 # Get the value of the child.
431 value
= firstchild
.nodeValue
437 class DDNSProviderAllInkl(DDNSProvider
):
438 handle
= "all-inkl.com"
439 name
= "All-inkl.com"
440 website
= "http://all-inkl.com/"
441 protocols
= ("ipv4",)
443 # There are only information provided by the vendor how to
444 # perform an update on a FRITZ Box. Grab requried informations
446 # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
448 url
= "http://dyndns.kasserver.com"
449 can_remove_records
= False
452 # There is no additional data required so we directly can
454 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
)
456 # Get the full response message.
457 output
= response
.read()
459 # Handle success messages.
460 if output
.startswith("good") or output
.startswith("nochg"):
463 # If we got here, some other update error happened.
464 raise DDNSUpdateError
467 class DDNSProviderBindNsupdate(DDNSProvider
):
469 name
= "BIND nsupdate utility"
470 website
= "http://en.wikipedia.org/wiki/Nsupdate"
476 # Search if the nsupdate utility is available
477 paths
= os
.environ
.get("PATH")
479 for path
in paths
.split(":"):
480 executable
= os
.path
.join(path
, "nsupdate")
482 if os
.path
.exists(executable
):
488 scriptlet
= self
.__make
_scriptlet
()
490 # -v enables TCP hence we transfer keys and other data that may
491 # exceed the size of one packet.
492 # -t sets the timeout
493 command
= ["nsupdate", "-v", "-t", "60"]
495 p
= subprocess
.Popen(command
, shell
=True,
496 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
498 stdout
, stderr
= p
.communicate(scriptlet
)
500 if p
.returncode
== 0:
503 raise DDNSError("nsupdate terminated with error code: %s\n %s" % (p
.returncode
, stderr
))
505 def __make_scriptlet(self
):
508 # Set a different server the update is sent to.
509 server
= self
.get("server", None)
511 scriptlet
.append("server %s" % server
)
513 # Set the DNS zone the host should be added to.
514 zone
= self
.get("zone", None)
516 scriptlet
.append("zone %s" % zone
)
518 key
= self
.get("key", None)
520 secret
= self
.get("secret")
522 scriptlet
.append("key %s %s" % (key
, secret
))
524 ttl
= self
.get("ttl", self
.DEFAULT_TTL
)
526 # Perform an update for each supported protocol.
527 for rrtype
, proto
in (("AAAA", "ipv6"), ("A", "ipv4")):
528 address
= self
.get_address(proto
)
532 scriptlet
.append("update delete %s. %s" % (self
.hostname
, rrtype
))
533 scriptlet
.append("update add %s. %s %s %s" % \
534 (self
.hostname
, ttl
, rrtype
, address
))
536 # Send the actions to the server.
537 scriptlet
.append("send")
538 scriptlet
.append("quit")
540 logger
.debug(_("Scriptlet:"))
541 for line
in scriptlet
:
542 # Masquerade the line with the secret key.
543 if line
.startswith("key"):
544 line
= "key **** ****"
546 logger
.debug(" %s" % line
)
548 return "\n".join(scriptlet
)
551 class DDNSProviderChangeIP(DDNSProvider
):
552 handle
= "changeip.com"
553 name
= "ChangeIP.com"
554 website
= "https://changeip.com"
555 protocols
= ("ipv4",)
557 # Detailed information about the update api can be found here.
558 # http://www.changeip.com/accounts/knowledgebase.php?action=displayarticle&id=34
560 url
= "https://nic.changeip.com/nic/update"
561 can_remove_records
= False
563 def update_protocol(self
, proto
):
565 "hostname" : self
.hostname
,
566 "myip" : self
.get_address(proto
),
569 # Send update to the server.
571 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
574 # Handle error codes.
575 except urllib2
.HTTPError
, e
:
577 raise DDNSRequestError(_("Domain not found."))
581 # Handle success message.
582 if response
.code
== 200:
585 # If we got here, some other update error happened.
586 raise DDNSUpdateError(_("Server response: %s") % output
)
589 class DDNSProviderDDNSS(DDNSProvider
):
592 website
= "http://www.ddnss.de"
593 protocols
= ("ipv4",)
595 # Detailed information about how to send the update request and possible response
596 # codes can be obtained from here.
597 # http://www.ddnss.de/info.php
598 # http://www.megacomputing.de/2014/08/dyndns-service-response-time/#more-919
600 url
= "http://www.ddnss.de/upd.php"
601 can_remove_records
= False
603 def update_protocol(self
, proto
):
605 "ip" : self
.get_address(proto
),
606 "host" : self
.hostname
,
609 # Check if a token has been set.
611 data
["key"] = self
.token
613 # Check if username and hostname are given.
614 elif self
.username
and self
.password
:
616 "user" : self
.username
,
617 "pwd" : self
.password
,
620 # Raise an error if no auth details are given.
622 raise DDNSConfigurationError
624 # Send update to the server.
625 response
= self
.send_request(self
.url
, data
=data
)
627 # This provider sends the response code as part of the header.
628 header
= response
.info()
630 # Get status information from the header.
631 output
= header
.getheader('ddnss-response')
633 # Handle success messages.
634 if output
== "good" or output
== "nochg":
637 # Handle error codes.
638 if output
== "badauth":
639 raise DDNSAuthenticationError
640 elif output
== "notfqdn":
641 raise DDNSRequestError(_("No valid FQDN was given."))
642 elif output
== "nohost":
643 raise DDNSRequestError(_("Specified host does not exist."))
644 elif output
== "911":
645 raise DDNSInternalServerError
646 elif output
== "dnserr":
647 raise DDNSInternalServerError(_("DNS error encountered."))
648 elif output
== "disabled":
649 raise DDNSRequestError(_("Account disabled or locked."))
651 # If we got here, some other update error happened.
652 raise DDNSUpdateError
655 class DDNSProviderDHS(DDNSProvider
):
657 name
= "DHS International"
658 website
= "http://dhs.org/"
659 protocols
= ("ipv4",)
661 # No information about the used update api provided on webpage,
662 # grabed from source code of ez-ipudate.
664 url
= "http://members.dhs.org/nic/hosts"
665 can_remove_records
= False
667 def update_protocol(self
, proto
):
669 "domain" : self
.hostname
,
670 "ip" : self
.get_address(proto
),
672 "hostcmdstage" : "2",
676 # Send update to the server.
677 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
680 # Handle success messages.
681 if response
.code
== 200:
684 # If we got here, some other update error happened.
685 raise DDNSUpdateError
688 class DDNSProviderDNSpark(DDNSProvider
):
689 handle
= "dnspark.com"
691 website
= "http://dnspark.com/"
692 protocols
= ("ipv4",)
694 # Informations to the used api can be found here:
695 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
697 url
= "https://control.dnspark.com/api/dynamic/update.php"
698 can_remove_records
= False
700 def update_protocol(self
, proto
):
702 "domain" : self
.hostname
,
703 "ip" : self
.get_address(proto
),
706 # Send update to the server.
707 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
710 # Get the full response message.
711 output
= response
.read()
713 # Handle success messages.
714 if output
.startswith("ok") or output
.startswith("nochange"):
717 # Handle error codes.
718 if output
== "unauth":
719 raise DDNSAuthenticationError
720 elif output
== "abuse":
722 elif output
== "blocked":
723 raise DDNSBlockedError
724 elif output
== "nofqdn":
725 raise DDNSRequestError(_("No valid FQDN was given."))
726 elif output
== "nohost":
727 raise DDNSRequestError(_("Invalid hostname specified."))
728 elif output
== "notdyn":
729 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
730 elif output
== "invalid":
731 raise DDNSRequestError(_("Invalid IP address has been sent."))
733 # If we got here, some other update error happened.
734 raise DDNSUpdateError
737 class DDNSProviderDtDNS(DDNSProvider
):
740 website
= "http://dtdns.com/"
741 protocols
= ("ipv4",)
743 # Information about the format of the HTTPS request is to be found
744 # http://www.dtdns.com/dtsite/updatespec
746 url
= "https://www.dtdns.com/api/autodns.cfm"
747 can_remove_records
= False
749 def update_protocol(self
, proto
):
751 "ip" : self
.get_address(proto
),
752 "id" : self
.hostname
,
756 # Send update to the server.
757 response
= self
.send_request(self
.url
, data
=data
)
759 # Get the full response message.
760 output
= response
.read()
762 # Remove all leading and trailing whitespace.
763 output
= output
.strip()
765 # Handle success messages.
766 if "now points to" in output
:
769 # Handle error codes.
770 if output
== "No hostname to update was supplied.":
771 raise DDNSRequestError(_("No hostname specified."))
773 elif output
== "The hostname you supplied is not valid.":
774 raise DDNSRequestError(_("Invalid hostname specified."))
776 elif output
== "The password you supplied is not valid.":
777 raise DDNSAuthenticationError
779 elif output
== "Administration has disabled this account.":
780 raise DDNSRequestError(_("Account has been disabled."))
782 elif output
== "Illegal character in IP.":
783 raise DDNSRequestError(_("Invalid IP address has been sent."))
785 elif output
== "Too many failed requests.":
786 raise DDNSRequestError(_("Too many failed requests."))
788 # If we got here, some other update error happened.
789 raise DDNSUpdateError
792 class DDNSProviderDynDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
793 handle
= "dyndns.org"
795 website
= "http://dyn.com/dns/"
796 protocols
= ("ipv4",)
798 # Information about the format of the request is to be found
799 # http://http://dyn.com/support/developers/api/perform-update/
800 # http://dyn.com/support/developers/api/return-codes/
802 url
= "https://members.dyndns.org/nic/update"
805 class DDNSProviderDynU(DDNSProtocolDynDNS2
, DDNSProvider
):
808 website
= "http://dynu.com/"
809 protocols
= ("ipv6", "ipv4",)
811 # Detailed information about the request and response codes
812 # are available on the providers webpage.
813 # http://dynu.com/Default.aspx?page=dnsapi
815 url
= "https://api.dynu.com/nic/update"
817 # DynU sends the IPv6 and IPv4 address in one request
820 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, "ipv4")
822 # This one supports IPv6
823 myipv6
= self
.get_address("ipv6")
825 # Add update information if we have an IPv6 address.
827 data
["myipv6"] = myipv6
829 self
.send_request(data
)
832 class DDNSProviderEasyDNS(DDNSProvider
):
833 handle
= "easydns.com"
835 website
= "http://www.easydns.com/"
836 protocols
= ("ipv4",)
838 # Detailed information about the request and response codes
839 # (API 1.3) are available on the providers webpage.
840 # https://fusion.easydns.com/index.php?/Knowledgebase/Article/View/102/7/dynamic-dns
842 url
= "http://api.cp.easydns.com/dyn/tomato.php"
844 def update_protocol(self
, proto
):
846 "myip" : self
.get_address(proto
, "-"),
847 "hostname" : self
.hostname
,
850 # Send update to the server.
851 response
= self
.send_request(self
.url
, data
=data
,
852 username
=self
.username
, password
=self
.password
)
854 # Get the full response message.
855 output
= response
.read()
857 # Remove all leading and trailing whitespace.
858 output
= output
.strip()
860 # Handle success messages.
861 if output
.startswith("NOERROR"):
864 # Handle error codes.
865 if output
.startswith("NOACCESS"):
866 raise DDNSAuthenticationError
868 elif output
.startswith("NOSERVICE"):
869 raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain."))
871 elif output
.startswith("ILLEGAL INPUT"):
872 raise DDNSRequestError(_("Invalid data has been sent."))
874 elif output
.startswith("TOOSOON"):
875 raise DDNSRequestError(_("Too frequent update requests have been sent."))
877 # If we got here, some other update error happened.
878 raise DDNSUpdateError
881 class DDNSProviderDomopoli(DDNSProtocolDynDNS2
, DDNSProvider
):
882 handle
= "domopoli.de"
884 website
= "http://domopoli.de/"
885 protocols
= ("ipv4",)
887 # https://www.domopoli.de/?page=howto#DynDns_start
889 url
= "http://dyndns.domopoli.de/nic/update"
892 class DDNSProviderDynsNet(DDNSProvider
):
895 website
= "http://www.dyns.net/"
896 protocols
= ("ipv4",)
897 can_remove_records
= False
899 # There is very detailed informatio about how to send the update request and
900 # the possible response codes. (Currently we are using the v1.1 proto)
901 # http://www.dyns.net/documentation/technical/protocol/
903 url
= "http://www.dyns.net/postscript011.php"
905 def update_protocol(self
, proto
):
907 "ip" : self
.get_address(proto
),
908 "host" : self
.hostname
,
909 "username" : self
.username
,
910 "password" : self
.password
,
913 # Send update to the server.
914 response
= self
.send_request(self
.url
, data
=data
)
916 # Get the full response message.
917 output
= response
.read()
919 # Handle success messages.
920 if output
.startswith("200"):
923 # Handle error codes.
924 if output
.startswith("400"):
925 raise DDNSRequestError(_("Malformed request has been sent."))
926 elif output
.startswith("401"):
927 raise DDNSAuthenticationError
928 elif output
.startswith("402"):
929 raise DDNSRequestError(_("Too frequent update requests have been sent."))
930 elif output
.startswith("403"):
931 raise DDNSInternalServerError
933 # If we got here, some other update error happened.
934 raise DDNSUpdateError(_("Server response: %s") % output
)
937 class DDNSProviderEnomCom(DDNSResponseParserXML
, DDNSProvider
):
940 website
= "http://www.enom.com/"
941 protocols
= ("ipv4",)
943 # There are very detailed information about how to send an update request and
945 # http://www.enom.com/APICommandCatalog/
947 url
= "https://dynamic.name-services.com/interface.asp"
948 can_remove_records
= False
950 def update_protocol(self
, proto
):
952 "command" : "setdnshost",
953 "responsetype" : "xml",
954 "address" : self
.get_address(proto
),
955 "domainpassword" : self
.password
,
956 "zone" : self
.hostname
959 # Send update to the server.
960 response
= self
.send_request(self
.url
, data
=data
)
962 # Get the full response message.
963 output
= response
.read()
965 # Handle success messages.
966 if self
.get_xml_tag_value(output
, "ErrCount") == "0":
969 # Handle error codes.
970 errorcode
= self
.get_xml_tag_value(output
, "ResponseNumber")
972 if errorcode
== "304155":
973 raise DDNSAuthenticationError
974 elif errorcode
== "304153":
975 raise DDNSRequestError(_("Domain not found."))
977 # If we got here, some other update error happened.
978 raise DDNSUpdateError
981 class DDNSProviderEntryDNS(DDNSProvider
):
982 handle
= "entrydns.net"
984 website
= "http://entrydns.net/"
985 protocols
= ("ipv4",)
987 # Some very tiny details about their so called "Simple API" can be found
988 # here: https://entrydns.net/help
989 url
= "https://entrydns.net/records/modify"
990 can_remove_records
= False
992 def update_protocol(self
, proto
):
994 "ip" : self
.get_address(proto
),
997 # Add auth token to the update url.
998 url
= "%s/%s" % (self
.url
, self
.token
)
1000 # Send update to the server.
1002 response
= self
.send_request(url
, data
=data
)
1004 # Handle error codes
1005 except urllib2
.HTTPError
, e
:
1007 raise DDNSAuthenticationError
1010 raise DDNSRequestError(_("An invalid IP address was submitted"))
1014 # Handle success messages.
1015 if response
.code
== 200:
1018 # If we got here, some other update error happened.
1019 raise DDNSUpdateError
1022 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider
):
1023 handle
= "freedns.afraid.org"
1024 name
= "freedns.afraid.org"
1025 website
= "http://freedns.afraid.org/"
1027 # No information about the request or response could be found on the vendor
1028 # page. All used values have been collected by testing.
1029 url
= "https://freedns.afraid.org/dynamic/update.php"
1030 can_remove_records
= False
1032 def update_protocol(self
, proto
):
1034 "address" : self
.get_address(proto
),
1037 # Add auth token to the update url.
1038 url
= "%s?%s" % (self
.url
, self
.token
)
1040 # Send update to the server.
1041 response
= self
.send_request(url
, data
=data
)
1043 # Get the full response message.
1044 output
= response
.read()
1046 # Handle success messages.
1047 if output
.startswith("Updated") or "has not changed" in output
:
1050 # Handle error codes.
1051 if output
== "ERROR: Unable to locate this record":
1052 raise DDNSAuthenticationError
1053 elif "is an invalid IP address" in output
:
1054 raise DDNSRequestError(_("Invalid IP address has been sent."))
1056 # If we got here, some other update error happened.
1057 raise DDNSUpdateError
1060 class DDNSProviderJoker(DDNSProtocolDynDNS2
, DDNSProvider
):
1061 handle
= "joker.com"
1062 name
= "Joker.com Dynamic DNS"
1063 website
= "https://joker.com/"
1064 protocols
= ("ipv4",)
1066 # Information about the request can be found here:
1067 # https://joker.com/faq/content/11/427/en/what-is-dynamic-dns-dyndns.html
1068 # Using DynDNS V2 protocol over HTTPS here
1070 url
= "https://svc.joker.com/nic/update"
1073 class DDNSProviderGoogle(DDNSProtocolDynDNS2
, DDNSProvider
):
1074 handle
= "domains.google.com"
1075 name
= "Google Domains"
1076 website
= "https://domains.google.com/"
1077 protocols
= ("ipv4",)
1079 # Information about the format of the HTTP request is to be found
1080 # here: https://support.google.com/domains/answer/6147083?hl=en
1082 url
= "https://domains.google.com/nic/update"
1085 class DDNSProviderLightningWireLabs(DDNSProvider
):
1086 handle
= "dns.lightningwirelabs.com"
1087 name
= "Lightning Wire Labs DNS Service"
1088 website
= "http://dns.lightningwirelabs.com/"
1090 # Information about the format of the HTTPS request is to be found
1091 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
1093 url
= "https://dns.lightningwirelabs.com/update"
1097 "hostname" : self
.hostname
,
1098 "address6" : self
.get_address("ipv6", "-"),
1099 "address4" : self
.get_address("ipv4", "-"),
1102 # Check if a token has been set.
1104 data
["token"] = self
.token
1106 # Check for username and password.
1107 elif self
.username
and self
.password
:
1109 "username" : self
.username
,
1110 "password" : self
.password
,
1113 # Raise an error if no auth details are given.
1115 raise DDNSConfigurationError
1117 # Send update to the server.
1118 response
= self
.send_request(self
.url
, data
=data
)
1120 # Handle success messages.
1121 if response
.code
== 200:
1124 # If we got here, some other update error happened.
1125 raise DDNSUpdateError
1128 class DDNSProviderLoopia(DDNSProtocolDynDNS2
, DDNSProvider
):
1129 handle
= "loopia.se"
1131 website
= "https://www.loopia.com"
1132 protocols
= ("ipv4",)
1134 # Information about the format of the HTTP request is to be found
1135 # here: https://support.loopia.com/wiki/About_the_DynDNS_support
1137 url
= "https://dns.loopia.se/XDynDNSServer/XDynDNS.php"
1140 class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2
, DDNSProvider
):
1141 handle
= "myonlineportal.net"
1142 name
= "myonlineportal.net"
1143 website
= "https:/myonlineportal.net/"
1145 # Information about the request and response can be obtained here:
1146 # https://myonlineportal.net/howto_dyndns
1148 url
= "https://myonlineportal.net/updateddns"
1150 def prepare_request_data(self
, proto
):
1152 "hostname" : self
.hostname
,
1153 "ip" : self
.get_address(proto
),
1159 class DDNSProviderNamecheap(DDNSResponseParserXML
, DDNSProvider
):
1160 handle
= "namecheap.com"
1162 website
= "http://namecheap.com"
1163 protocols
= ("ipv4",)
1165 # Information about the format of the HTTP request is to be found
1166 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
1167 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
1169 url
= "https://dynamicdns.park-your-domain.com/update"
1170 can_remove_records
= False
1172 def update_protocol(self
, proto
):
1173 # Namecheap requires the hostname splitted into a host and domain part.
1174 host
, domain
= self
.hostname
.split(".", 1)
1176 # Get and store curent IP address.
1177 address
= self
.get_address(proto
)
1181 "password" : self
.password
,
1186 # Send update to the server.
1187 response
= self
.send_request(self
.url
, data
=data
)
1189 # Get the full response message.
1190 output
= response
.read()
1192 # Handle success messages.
1193 if self
.get_xml_tag_value(output
, "IP") == address
:
1196 # Handle error codes.
1197 errorcode
= self
.get_xml_tag_value(output
, "ResponseNumber")
1199 if errorcode
== "304156":
1200 raise DDNSAuthenticationError
1201 elif errorcode
== "316153":
1202 raise DDNSRequestError(_("Domain not found."))
1203 elif errorcode
== "316154":
1204 raise DDNSRequestError(_("Domain not active."))
1205 elif errorcode
in ("380098", "380099"):
1206 raise DDNSInternalServerError
1208 # If we got here, some other update error happened.
1209 raise DDNSUpdateError
1212 class DDNSProviderNOIP(DDNSProtocolDynDNS2
, DDNSProvider
):
1213 handle
= "no-ip.com"
1215 website
= "http://www.no-ip.com/"
1216 protocols
= ("ipv4",)
1218 # Information about the format of the HTTP request is to be found
1219 # here: http://www.no-ip.com/integrate/request and
1220 # here: http://www.no-ip.com/integrate/response
1222 url
= "http://dynupdate.no-ip.com/nic/update"
1224 def prepare_request_data(self
, proto
):
1225 assert proto
== "ipv4"
1228 "hostname" : self
.hostname
,
1229 "address" : self
.get_address(proto
),
1235 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2
, DDNSProvider
):
1236 handle
= "nsupdate.info"
1237 name
= "nsupdate.info"
1238 website
= "http://nsupdate.info/"
1239 protocols
= ("ipv6", "ipv4",)
1241 # Information about the format of the HTTP request can be found
1242 # after login on the provider user interface and here:
1243 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1245 url
= "https://nsupdate.info/nic/update"
1247 # TODO nsupdate.info can actually do this, but the functionality
1248 # has not been implemented here, yet.
1249 can_remove_records
= False
1251 # After a failed update, there will be no retries
1252 # https://bugzilla.ipfire.org/show_bug.cgi?id=10603
1253 holdoff_failure_days
= None
1255 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1256 # and for the password a so called secret.
1259 return self
.get("hostname")
1263 return self
.token
or self
.get("secret")
1265 def prepare_request_data(self
, proto
):
1267 "myip" : self
.get_address(proto
),
1273 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
1274 handle
= "opendns.com"
1276 website
= "http://www.opendns.com"
1278 # Detailed information about the update request and possible
1279 # response codes can be obtained from here:
1280 # https://support.opendns.com/entries/23891440
1282 url
= "https://updates.opendns.com/nic/update"
1284 def prepare_request_data(self
, proto
):
1286 "hostname" : self
.hostname
,
1287 "myip" : self
.get_address(proto
),
1293 class DDNSProviderOVH(DDNSProtocolDynDNS2
, DDNSProvider
):
1296 website
= "http://www.ovh.com/"
1297 protocols
= ("ipv4",)
1299 # OVH only provides very limited information about how to
1300 # update a DynDNS host. They only provide the update url
1301 # on the their german subpage.
1303 # http://hilfe.ovh.de/DomainDynHost
1305 url
= "https://www.ovh.com/nic/update"
1307 def prepare_request_data(self
, proto
):
1308 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, proto
)
1310 "system" : "dyndns",
1316 class DDNSProviderRegfish(DDNSProvider
):
1317 handle
= "regfish.com"
1318 name
= "Regfish GmbH"
1319 website
= "http://www.regfish.com/"
1321 # A full documentation to the providers api can be found here
1322 # but is only available in german.
1323 # https://www.regfish.de/domains/dyndns/dokumentation
1325 url
= "https://dyndns.regfish.de/"
1326 can_remove_records
= False
1330 "fqdn" : self
.hostname
,
1333 # Check if we update an IPv6 address.
1334 address6
= self
.get_address("ipv6")
1336 data
["ipv6"] = address6
1338 # Check if we update an IPv4 address.
1339 address4
= self
.get_address("ipv4")
1341 data
["ipv4"] = address4
1343 # Raise an error if none address is given.
1344 if not data
.has_key("ipv6") and not data
.has_key("ipv4"):
1345 raise DDNSConfigurationError
1347 # Check if a token has been set.
1349 data
["token"] = self
.token
1351 # Raise an error if no token and no useranem and password
1353 elif not self
.username
and not self
.password
:
1354 raise DDNSConfigurationError(_("No Auth details specified."))
1356 # HTTP Basic Auth is only allowed if no token is used.
1358 # Send update to the server.
1359 response
= self
.send_request(self
.url
, data
=data
)
1361 # Send update to the server.
1362 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
1365 # Get the full response message.
1366 output
= response
.read()
1368 # Handle success messages.
1369 if "100" in output
or "101" in output
:
1372 # Handle error codes.
1373 if "401" or "402" in output
:
1374 raise DDNSAuthenticationError
1375 elif "408" in output
:
1376 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1377 elif "409" in output
:
1378 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1379 elif "412" in output
:
1380 raise DDNSRequestError(_("No valid FQDN was given."))
1381 elif "414" in output
:
1382 raise DDNSInternalServerError
1384 # If we got here, some other update error happened.
1385 raise DDNSUpdateError
1388 class DDNSProviderSelfhost(DDNSProtocolDynDNS2
, DDNSProvider
):
1389 handle
= "selfhost.de"
1390 name
= "Selfhost.de"
1391 website
= "http://www.selfhost.de/"
1392 protocols
= ("ipv4",)
1394 url
= "https://carol.selfhost.de/nic/update"
1396 def prepare_request_data(self
, proto
):
1397 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, proto
)
1405 class DDNSProviderSPDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
1406 handle
= "spdns.org"
1408 website
= "http://spdns.org/"
1410 # Detailed information about request and response codes are provided
1411 # by the vendor. They are using almost the same mechanism and status
1412 # codes as dyndns.org so we can inherit all those stuff.
1414 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1415 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1417 url
= "https://update.spdns.de/nic/update"
1421 return self
.get("username") or self
.hostname
1425 return self
.get("password") or self
.token
1428 class DDNSProviderStrato(DDNSProtocolDynDNS2
, DDNSProvider
):
1429 handle
= "strato.com"
1431 website
= "http:/www.strato.com/"
1432 protocols
= ("ipv4",)
1434 # Information about the request and response can be obtained here:
1435 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1437 url
= "https://dyndns.strato.com/nic/update"
1439 def prepare_request_data(self
, proto
):
1440 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, proto
)
1443 "backupmx" : "NOCHG"
1449 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
1450 handle
= "twodns.de"
1452 website
= "http://www.twodns.de"
1453 protocols
= ("ipv4",)
1455 # Detailed information about the request can be found here
1456 # http://twodns.de/en/faqs
1457 # http://twodns.de/en/api
1459 url
= "https://update.twodns.de/update"
1461 def prepare_request_data(self
, proto
):
1462 assert proto
== "ipv4"
1465 "ip" : self
.get_address(proto
),
1466 "hostname" : self
.hostname
1472 class DDNSProviderUdmedia(DDNSProtocolDynDNS2
, DDNSProvider
):
1473 handle
= "udmedia.de"
1474 name
= "Udmedia GmbH"
1475 website
= "http://www.udmedia.de"
1476 protocols
= ("ipv4",)
1478 # Information about the request can be found here
1479 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1481 url
= "https://www.udmedia.de/nic/update"
1484 class DDNSProviderVariomedia(DDNSProtocolDynDNS2
, DDNSProvider
):
1485 handle
= "variomedia.de"
1487 website
= "http://www.variomedia.de/"
1488 protocols
= ("ipv6", "ipv4",)
1490 # Detailed information about the request can be found here
1491 # https://dyndns.variomedia.de/
1493 url
= "https://dyndns.variomedia.de/nic/update"
1495 def prepare_request_data(self
, proto
):
1497 "hostname" : self
.hostname
,
1498 "myip" : self
.get_address(proto
),
1504 class DDNSProviderXLhost(DDNSProtocolDynDNS2
, DDNSProvider
):
1505 handle
= "xlhost.de"
1507 website
= "http://xlhost.de/"
1508 protocols
= ("ipv4",)
1510 # Information about the format of the HTTP request is to be found
1511 # here: https://xlhost.de/faq/index_html?topicId=CQA2ELIPO4SQ
1513 url
= "https://nsupdate.xlhost.de/"
1516 class DDNSProviderZoneedit(DDNSProvider
):
1517 handle
= "zoneedit.com"
1519 website
= "http://www.zoneedit.com"
1520 protocols
= ("ipv4",)
1522 # Detailed information about the request and the response codes can be
1524 # http://www.zoneedit.com/doc/api/other.html
1525 # http://www.zoneedit.com/faq.html
1527 url
= "https://dynamic.zoneedit.com/auth/dynamic.html"
1529 def update_protocol(self
, proto
):
1531 "dnsto" : self
.get_address(proto
),
1532 "host" : self
.hostname
1535 # Send update to the server.
1536 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
1539 # Get the full response message.
1540 output
= response
.read()
1542 # Handle success messages.
1543 if output
.startswith("<SUCCESS"):
1546 # Handle error codes.
1547 if output
.startswith("invalid login"):
1548 raise DDNSAuthenticationError
1549 elif output
.startswith("<ERROR CODE=\"704\""):
1550 raise DDNSRequestError(_("No valid FQDN was given."))
1551 elif output
.startswith("<ERROR CODE=\"702\""):
1552 raise DDNSInternalServerError
1554 # If we got here, some other update error happened.
1555 raise DDNSUpdateError
1558 class DDNSProviderDNSmadeEasy(DDNSProvider
):
1559 handle
= "dnsmadeeasy.com"
1560 name
= "DNSmadeEasy.com"
1561 website
= "http://www.dnsmadeeasy.com/"
1562 protocols
= ("ipv4",)
1564 # DNS Made Easy Nameserver Provider also offering Dynamic DNS
1565 # Documentation can be found here:
1566 # http://www.dnsmadeeasy.com/dynamic-dns/
1568 url
= "https://cp.dnsmadeeasy.com/servlet/updateip?"
1569 can_remove_records
= False
1571 def update_protocol(self
, proto
):
1573 "ip" : self
.get_address(proto
),
1574 "id" : self
.hostname
,
1575 "username" : self
.username
,
1576 "password" : self
.password
,
1579 # Send update to the server.
1580 response
= self
.send_request(self
.url
, data
=data
)
1582 # Get the full response message.
1583 output
= response
.read()
1585 # Handle success messages.
1586 if output
.startswith("success") or output
.startswith("error-record-ip-same"):
1589 # Handle error codes.
1590 if output
.startswith("error-auth-suspend"):
1591 raise DDNSRequestError(_("Account has been suspended."))
1593 elif output
.startswith("error-auth-voided"):
1594 raise DDNSRequestError(_("Account has been revoked."))
1596 elif output
.startswith("error-record-invalid"):
1597 raise DDNSRequestError(_("Specified host does not exist."))
1599 elif output
.startswith("error-auth"):
1600 raise DDNSAuthenticationError
1602 # If we got here, some other update error happened.
1603 raise DDNSUpdateError(_("Server response: %s") % output
)
1606 class DDNSProviderZZZZ(DDNSProvider
):
1609 website
= "https://zzzz.io"
1610 protocols
= ("ipv6", "ipv4",)
1612 # Detailed information about the update request can be found here:
1613 # https://zzzz.io/faq/
1615 # Details about the possible response codes have been provided in the bugtracker:
1616 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1618 url
= "https://zzzz.io/api/v1/update"
1619 can_remove_records
= False
1621 def update_protocol(self
, proto
):
1623 "ip" : self
.get_address(proto
),
1624 "token" : self
.token
,
1628 data
["type"] = "aaaa"
1630 # zzzz uses the host from the full hostname as part
1631 # of the update url.
1632 host
, domain
= self
.hostname
.split(".", 1)
1634 # Add host value to the update url.
1635 url
= "%s/%s" % (self
.url
, host
)
1637 # Send update to the server.
1639 response
= self
.send_request(url
, data
=data
)
1641 # Handle error codes.
1642 except DDNSNotFound
:
1643 raise DDNSRequestError(_("Invalid hostname specified"))
1645 # Handle success messages.
1646 if response
.code
== 200:
1649 # If we got here, some other update error happened.
1650 raise DDNSUpdateError