]>
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 # 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 DDNSProviderDesecIO(DDNSProtocolDynDNS2
, DDNSProvider
):
592 website
= "https://www.desec.io"
593 protocols
= ("ipv6", "ipv4",)
595 # ipv4 / ipv6 records are automatically removed when the update
596 # request originates from the respectively other protocol and no
597 # address is explicitly provided for the unused protocol.
599 url
= "https://update.dedyn.io"
601 # desec.io sends the IPv6 and IPv4 address in one request
604 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, "ipv4")
606 # This one supports IPv6
607 myipv6
= self
.get_address("ipv6")
609 # Add update information if we have an IPv6 address.
611 data
["myipv6"] = myipv6
613 self
.send_request(data
)
616 class DDNSProviderDDNSS(DDNSProvider
):
619 website
= "http://www.ddnss.de"
620 protocols
= ("ipv4",)
622 # Detailed information about how to send the update request and possible response
623 # codes can be obtained from here.
624 # http://www.ddnss.de/info.php
625 # http://www.megacomputing.de/2014/08/dyndns-service-response-time/#more-919
627 url
= "http://www.ddnss.de/upd.php"
628 can_remove_records
= False
630 def update_protocol(self
, proto
):
632 "ip" : self
.get_address(proto
),
633 "host" : self
.hostname
,
636 # Check if a token has been set.
638 data
["key"] = self
.token
640 # Check if username and hostname are given.
641 elif self
.username
and self
.password
:
643 "user" : self
.username
,
644 "pwd" : self
.password
,
647 # Raise an error if no auth details are given.
649 raise DDNSConfigurationError
651 # Send update to the server.
652 response
= self
.send_request(self
.url
, data
=data
)
654 # This provider sends the response code as part of the header.
655 header
= response
.info()
657 # Get status information from the header.
658 output
= header
.getheader('ddnss-response')
660 # Handle success messages.
661 if output
== "good" or output
== "nochg":
664 # Handle error codes.
665 if output
== "badauth":
666 raise DDNSAuthenticationError
667 elif output
== "notfqdn":
668 raise DDNSRequestError(_("No valid FQDN was given."))
669 elif output
== "nohost":
670 raise DDNSRequestError(_("Specified host does not exist."))
671 elif output
== "911":
672 raise DDNSInternalServerError
673 elif output
== "dnserr":
674 raise DDNSInternalServerError(_("DNS error encountered."))
675 elif output
== "disabled":
676 raise DDNSRequestError(_("Account disabled or locked."))
678 # If we got here, some other update error happened.
679 raise DDNSUpdateError
682 class DDNSProviderDHS(DDNSProvider
):
684 name
= "DHS International"
685 website
= "http://dhs.org/"
686 protocols
= ("ipv4",)
688 # No information about the used update api provided on webpage,
689 # grabed from source code of ez-ipudate.
691 url
= "http://members.dhs.org/nic/hosts"
692 can_remove_records
= False
694 def update_protocol(self
, proto
):
696 "domain" : self
.hostname
,
697 "ip" : self
.get_address(proto
),
699 "hostcmdstage" : "2",
703 # Send update to the server.
704 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
707 # Handle success messages.
708 if response
.code
== 200:
711 # If we got here, some other update error happened.
712 raise DDNSUpdateError
715 class DDNSProviderDNSpark(DDNSProvider
):
716 handle
= "dnspark.com"
718 website
= "http://dnspark.com/"
719 protocols
= ("ipv4",)
721 # Informations to the used api can be found here:
722 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
724 url
= "https://control.dnspark.com/api/dynamic/update.php"
725 can_remove_records
= False
727 def update_protocol(self
, proto
):
729 "domain" : self
.hostname
,
730 "ip" : self
.get_address(proto
),
733 # Send update to the server.
734 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
737 # Get the full response message.
738 output
= response
.read()
740 # Handle success messages.
741 if output
.startswith("ok") or output
.startswith("nochange"):
744 # Handle error codes.
745 if output
== "unauth":
746 raise DDNSAuthenticationError
747 elif output
== "abuse":
749 elif output
== "blocked":
750 raise DDNSBlockedError
751 elif output
== "nofqdn":
752 raise DDNSRequestError(_("No valid FQDN was given."))
753 elif output
== "nohost":
754 raise DDNSRequestError(_("Invalid hostname specified."))
755 elif output
== "notdyn":
756 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
757 elif output
== "invalid":
758 raise DDNSRequestError(_("Invalid IP address has been sent."))
760 # If we got here, some other update error happened.
761 raise DDNSUpdateError
764 class DDNSProviderDtDNS(DDNSProvider
):
767 website
= "http://dtdns.com/"
768 protocols
= ("ipv4",)
770 # Information about the format of the HTTPS request is to be found
771 # http://www.dtdns.com/dtsite/updatespec
773 url
= "https://www.dtdns.com/api/autodns.cfm"
774 can_remove_records
= False
776 def update_protocol(self
, proto
):
778 "ip" : self
.get_address(proto
),
779 "id" : self
.hostname
,
783 # Send update to the server.
784 response
= self
.send_request(self
.url
, data
=data
)
786 # Get the full response message.
787 output
= response
.read()
789 # Remove all leading and trailing whitespace.
790 output
= output
.strip()
792 # Handle success messages.
793 if "now points to" in output
:
796 # Handle error codes.
797 if output
== "No hostname to update was supplied.":
798 raise DDNSRequestError(_("No hostname specified."))
800 elif output
== "The hostname you supplied is not valid.":
801 raise DDNSRequestError(_("Invalid hostname specified."))
803 elif output
== "The password you supplied is not valid.":
804 raise DDNSAuthenticationError
806 elif output
== "Administration has disabled this account.":
807 raise DDNSRequestError(_("Account has been disabled."))
809 elif output
== "Illegal character in IP.":
810 raise DDNSRequestError(_("Invalid IP address has been sent."))
812 elif output
== "Too many failed requests.":
813 raise DDNSRequestError(_("Too many failed requests."))
815 # If we got here, some other update error happened.
816 raise DDNSUpdateError
819 class DDNSProviderDynDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
820 handle
= "dyndns.org"
822 website
= "http://dyn.com/dns/"
823 protocols
= ("ipv4",)
825 # Information about the format of the request is to be found
826 # http://http://dyn.com/support/developers/api/perform-update/
827 # http://dyn.com/support/developers/api/return-codes/
829 url
= "https://members.dyndns.org/nic/update"
832 class DDNSProviderDynU(DDNSProtocolDynDNS2
, DDNSProvider
):
835 website
= "http://dynu.com/"
836 protocols
= ("ipv6", "ipv4",)
838 # Detailed information about the request and response codes
839 # are available on the providers webpage.
840 # http://dynu.com/Default.aspx?page=dnsapi
842 url
= "https://api.dynu.com/nic/update"
844 # DynU sends the IPv6 and IPv4 address in one request
847 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, "ipv4")
849 # This one supports IPv6
850 myipv6
= self
.get_address("ipv6")
852 # Add update information if we have an IPv6 address.
854 data
["myipv6"] = myipv6
856 self
.send_request(data
)
859 class DDNSProviderEasyDNS(DDNSProvider
):
860 handle
= "easydns.com"
862 website
= "http://www.easydns.com/"
863 protocols
= ("ipv4",)
865 # Detailed information about the request and response codes
866 # (API 1.3) are available on the providers webpage.
867 # https://fusion.easydns.com/index.php?/Knowledgebase/Article/View/102/7/dynamic-dns
869 url
= "http://api.cp.easydns.com/dyn/tomato.php"
871 def update_protocol(self
, proto
):
873 "myip" : self
.get_address(proto
, "-"),
874 "hostname" : self
.hostname
,
877 # Send update to the server.
878 response
= self
.send_request(self
.url
, data
=data
,
879 username
=self
.username
, password
=self
.password
)
881 # Get the full response message.
882 output
= response
.read()
884 # Remove all leading and trailing whitespace.
885 output
= output
.strip()
887 # Handle success messages.
888 if output
.startswith("NOERROR"):
891 # Handle error codes.
892 if output
.startswith("NOACCESS"):
893 raise DDNSAuthenticationError
895 elif output
.startswith("NOSERVICE"):
896 raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain."))
898 elif output
.startswith("ILLEGAL INPUT"):
899 raise DDNSRequestError(_("Invalid data has been sent."))
901 elif output
.startswith("TOOSOON"):
902 raise DDNSRequestError(_("Too frequent update requests have been sent."))
904 # If we got here, some other update error happened.
905 raise DDNSUpdateError
908 class DDNSProviderDomopoli(DDNSProtocolDynDNS2
, DDNSProvider
):
909 handle
= "domopoli.de"
911 website
= "http://domopoli.de/"
912 protocols
= ("ipv4",)
914 # https://www.domopoli.de/?page=howto#DynDns_start
916 url
= "http://dyndns.domopoli.de/nic/update"
919 class DDNSProviderDynsNet(DDNSProvider
):
922 website
= "http://www.dyns.net/"
923 protocols
= ("ipv4",)
924 can_remove_records
= False
926 # There is very detailed informatio about how to send the update request and
927 # the possible response codes. (Currently we are using the v1.1 proto)
928 # http://www.dyns.net/documentation/technical/protocol/
930 url
= "http://www.dyns.net/postscript011.php"
932 def update_protocol(self
, proto
):
934 "ip" : self
.get_address(proto
),
935 "host" : self
.hostname
,
936 "username" : self
.username
,
937 "password" : self
.password
,
940 # Send update to the server.
941 response
= self
.send_request(self
.url
, data
=data
)
943 # Get the full response message.
944 output
= response
.read()
946 # Handle success messages.
947 if output
.startswith("200"):
950 # Handle error codes.
951 if output
.startswith("400"):
952 raise DDNSRequestError(_("Malformed request has been sent."))
953 elif output
.startswith("401"):
954 raise DDNSAuthenticationError
955 elif output
.startswith("402"):
956 raise DDNSRequestError(_("Too frequent update requests have been sent."))
957 elif output
.startswith("403"):
958 raise DDNSInternalServerError
960 # If we got here, some other update error happened.
961 raise DDNSUpdateError(_("Server response: %s") % output
)
964 class DDNSProviderEnomCom(DDNSResponseParserXML
, DDNSProvider
):
967 website
= "http://www.enom.com/"
968 protocols
= ("ipv4",)
970 # There are very detailed information about how to send an update request and
972 # http://www.enom.com/APICommandCatalog/
974 url
= "https://dynamic.name-services.com/interface.asp"
975 can_remove_records
= False
977 def update_protocol(self
, proto
):
979 "command" : "setdnshost",
980 "responsetype" : "xml",
981 "address" : self
.get_address(proto
),
982 "domainpassword" : self
.password
,
983 "zone" : self
.hostname
986 # Send update to the server.
987 response
= self
.send_request(self
.url
, data
=data
)
989 # Get the full response message.
990 output
= response
.read()
992 # Handle success messages.
993 if self
.get_xml_tag_value(output
, "ErrCount") == "0":
996 # Handle error codes.
997 errorcode
= self
.get_xml_tag_value(output
, "ResponseNumber")
999 if errorcode
== "304155":
1000 raise DDNSAuthenticationError
1001 elif errorcode
== "304153":
1002 raise DDNSRequestError(_("Domain not found."))
1004 # If we got here, some other update error happened.
1005 raise DDNSUpdateError
1008 class DDNSProviderEntryDNS(DDNSProvider
):
1009 handle
= "entrydns.net"
1011 website
= "http://entrydns.net/"
1012 protocols
= ("ipv4",)
1014 # Some very tiny details about their so called "Simple API" can be found
1015 # here: https://entrydns.net/help
1016 url
= "https://entrydns.net/records/modify"
1017 can_remove_records
= False
1019 def update_protocol(self
, proto
):
1021 "ip" : self
.get_address(proto
),
1024 # Add auth token to the update url.
1025 url
= "%s/%s" % (self
.url
, self
.token
)
1027 # Send update to the server.
1029 response
= self
.send_request(url
, data
=data
)
1031 # Handle error codes
1032 except urllib2
.HTTPError
, e
:
1034 raise DDNSAuthenticationError
1037 raise DDNSRequestError(_("An invalid IP address was submitted"))
1041 # Handle success messages.
1042 if response
.code
== 200:
1045 # If we got here, some other update error happened.
1046 raise DDNSUpdateError
1049 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider
):
1050 handle
= "freedns.afraid.org"
1051 name
= "freedns.afraid.org"
1052 website
= "http://freedns.afraid.org/"
1054 # No information about the request or response could be found on the vendor
1055 # page. All used values have been collected by testing.
1056 url
= "https://freedns.afraid.org/dynamic/update.php"
1057 can_remove_records
= False
1059 def update_protocol(self
, proto
):
1061 "address" : self
.get_address(proto
),
1064 # Add auth token to the update url.
1065 url
= "%s?%s" % (self
.url
, self
.token
)
1067 # Send update to the server.
1068 response
= self
.send_request(url
, data
=data
)
1070 # Get the full response message.
1071 output
= response
.read()
1073 # Handle success messages.
1074 if output
.startswith("Updated") or "has not changed" in output
:
1077 # Handle error codes.
1078 if output
== "ERROR: Unable to locate this record":
1079 raise DDNSAuthenticationError
1080 elif "is an invalid IP address" in output
:
1081 raise DDNSRequestError(_("Invalid IP address has been sent."))
1083 # If we got here, some other update error happened.
1084 raise DDNSUpdateError
1087 class DDNSProviderJoker(DDNSProtocolDynDNS2
, DDNSProvider
):
1088 handle
= "joker.com"
1089 name
= "Joker.com Dynamic DNS"
1090 website
= "https://joker.com/"
1091 protocols
= ("ipv4",)
1093 # Information about the request can be found here:
1094 # https://joker.com/faq/content/11/427/en/what-is-dynamic-dns-dyndns.html
1095 # Using DynDNS V2 protocol over HTTPS here
1097 url
= "https://svc.joker.com/nic/update"
1100 class DDNSProviderGoogle(DDNSProtocolDynDNS2
, DDNSProvider
):
1101 handle
= "domains.google.com"
1102 name
= "Google Domains"
1103 website
= "https://domains.google.com/"
1104 protocols
= ("ipv4",)
1106 # Information about the format of the HTTP request is to be found
1107 # here: https://support.google.com/domains/answer/6147083?hl=en
1109 url
= "https://domains.google.com/nic/update"
1112 class DDNSProviderLightningWireLabs(DDNSProvider
):
1113 handle
= "dns.lightningwirelabs.com"
1114 name
= "Lightning Wire Labs DNS Service"
1115 website
= "http://dns.lightningwirelabs.com/"
1117 # Information about the format of the HTTPS request is to be found
1118 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
1120 url
= "https://dns.lightningwirelabs.com/update"
1124 "hostname" : self
.hostname
,
1125 "address6" : self
.get_address("ipv6", "-"),
1126 "address4" : self
.get_address("ipv4", "-"),
1129 # Check if a token has been set.
1131 data
["token"] = self
.token
1133 # Check for username and password.
1134 elif self
.username
and self
.password
:
1136 "username" : self
.username
,
1137 "password" : self
.password
,
1140 # Raise an error if no auth details are given.
1142 raise DDNSConfigurationError
1144 # Send update to the server.
1145 response
= self
.send_request(self
.url
, data
=data
)
1147 # Handle success messages.
1148 if response
.code
== 200:
1151 # If we got here, some other update error happened.
1152 raise DDNSUpdateError
1155 class DDNSProviderLoopia(DDNSProtocolDynDNS2
, DDNSProvider
):
1156 handle
= "loopia.se"
1158 website
= "https://www.loopia.com"
1159 protocols
= ("ipv4",)
1161 # Information about the format of the HTTP request is to be found
1162 # here: https://support.loopia.com/wiki/About_the_DynDNS_support
1164 url
= "https://dns.loopia.se/XDynDNSServer/XDynDNS.php"
1167 class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2
, DDNSProvider
):
1168 handle
= "myonlineportal.net"
1169 name
= "myonlineportal.net"
1170 website
= "https:/myonlineportal.net/"
1172 # Information about the request and response can be obtained here:
1173 # https://myonlineportal.net/howto_dyndns
1175 url
= "https://myonlineportal.net/updateddns"
1177 def prepare_request_data(self
, proto
):
1179 "hostname" : self
.hostname
,
1180 "ip" : self
.get_address(proto
),
1186 class DDNSProviderNamecheap(DDNSResponseParserXML
, DDNSProvider
):
1187 handle
= "namecheap.com"
1189 website
= "http://namecheap.com"
1190 protocols
= ("ipv4",)
1192 # Information about the format of the HTTP request is to be found
1193 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
1194 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
1196 url
= "https://dynamicdns.park-your-domain.com/update"
1197 can_remove_records
= False
1199 def update_protocol(self
, proto
):
1200 # Namecheap requires the hostname splitted into a host and domain part.
1201 host
, domain
= self
.hostname
.split(".", 1)
1203 # Get and store curent IP address.
1204 address
= self
.get_address(proto
)
1208 "password" : self
.password
,
1213 # Send update to the server.
1214 response
= self
.send_request(self
.url
, data
=data
)
1216 # Get the full response message.
1217 output
= response
.read()
1219 # Handle success messages.
1220 if self
.get_xml_tag_value(output
, "IP") == address
:
1223 # Handle error codes.
1224 errorcode
= self
.get_xml_tag_value(output
, "ResponseNumber")
1226 if errorcode
== "304156":
1227 raise DDNSAuthenticationError
1228 elif errorcode
== "316153":
1229 raise DDNSRequestError(_("Domain not found."))
1230 elif errorcode
== "316154":
1231 raise DDNSRequestError(_("Domain not active."))
1232 elif errorcode
in ("380098", "380099"):
1233 raise DDNSInternalServerError
1235 # If we got here, some other update error happened.
1236 raise DDNSUpdateError
1239 class DDNSProviderNOIP(DDNSProtocolDynDNS2
, DDNSProvider
):
1240 handle
= "no-ip.com"
1242 website
= "http://www.no-ip.com/"
1243 protocols
= ("ipv4",)
1245 # Information about the format of the HTTP request is to be found
1246 # here: http://www.no-ip.com/integrate/request and
1247 # here: http://www.no-ip.com/integrate/response
1249 url
= "http://dynupdate.no-ip.com/nic/update"
1251 def prepare_request_data(self
, proto
):
1252 assert proto
== "ipv4"
1255 "hostname" : self
.hostname
,
1256 "address" : self
.get_address(proto
),
1262 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2
, DDNSProvider
):
1263 handle
= "nsupdate.info"
1264 name
= "nsupdate.info"
1265 website
= "http://nsupdate.info/"
1266 protocols
= ("ipv6", "ipv4",)
1268 # Information about the format of the HTTP request can be found
1269 # after login on the provider user interface and here:
1270 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1272 url
= "https://nsupdate.info/nic/update"
1274 # TODO nsupdate.info can actually do this, but the functionality
1275 # has not been implemented here, yet.
1276 can_remove_records
= False
1278 # After a failed update, there will be no retries
1279 # https://bugzilla.ipfire.org/show_bug.cgi?id=10603
1280 holdoff_failure_days
= None
1282 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1283 # and for the password a so called secret.
1286 return self
.get("hostname")
1290 return self
.token
or self
.get("secret")
1292 def prepare_request_data(self
, proto
):
1294 "myip" : self
.get_address(proto
),
1300 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
1301 handle
= "opendns.com"
1303 website
= "http://www.opendns.com"
1305 # Detailed information about the update request and possible
1306 # response codes can be obtained from here:
1307 # https://support.opendns.com/entries/23891440
1309 url
= "https://updates.opendns.com/nic/update"
1311 def prepare_request_data(self
, proto
):
1313 "hostname" : self
.hostname
,
1314 "myip" : self
.get_address(proto
),
1320 class DDNSProviderOVH(DDNSProtocolDynDNS2
, DDNSProvider
):
1323 website
= "http://www.ovh.com/"
1324 protocols
= ("ipv4",)
1326 # OVH only provides very limited information about how to
1327 # update a DynDNS host. They only provide the update url
1328 # on the their german subpage.
1330 # http://hilfe.ovh.de/DomainDynHost
1332 url
= "https://www.ovh.com/nic/update"
1334 def prepare_request_data(self
, proto
):
1335 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, proto
)
1337 "system" : "dyndns",
1343 class DDNSProviderRegfish(DDNSProvider
):
1344 handle
= "regfish.com"
1345 name
= "Regfish GmbH"
1346 website
= "http://www.regfish.com/"
1348 # A full documentation to the providers api can be found here
1349 # but is only available in german.
1350 # https://www.regfish.de/domains/dyndns/dokumentation
1352 url
= "https://dyndns.regfish.de/"
1353 can_remove_records
= False
1357 "fqdn" : self
.hostname
,
1360 # Check if we update an IPv6 address.
1361 address6
= self
.get_address("ipv6")
1363 data
["ipv6"] = address6
1365 # Check if we update an IPv4 address.
1366 address4
= self
.get_address("ipv4")
1368 data
["ipv4"] = address4
1370 # Raise an error if none address is given.
1371 if not data
.has_key("ipv6") and not data
.has_key("ipv4"):
1372 raise DDNSConfigurationError
1374 # Check if a token has been set.
1376 data
["token"] = self
.token
1378 # Raise an error if no token and no useranem and password
1380 elif not self
.username
and not self
.password
:
1381 raise DDNSConfigurationError(_("No Auth details specified."))
1383 # HTTP Basic Auth is only allowed if no token is used.
1385 # Send update to the server.
1386 response
= self
.send_request(self
.url
, data
=data
)
1388 # Send update to the server.
1389 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
1392 # Get the full response message.
1393 output
= response
.read()
1395 # Handle success messages.
1396 if "100" in output
or "101" in output
:
1399 # Handle error codes.
1400 if "401" or "402" in output
:
1401 raise DDNSAuthenticationError
1402 elif "408" in output
:
1403 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1404 elif "409" in output
:
1405 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1406 elif "412" in output
:
1407 raise DDNSRequestError(_("No valid FQDN was given."))
1408 elif "414" in output
:
1409 raise DDNSInternalServerError
1411 # If we got here, some other update error happened.
1412 raise DDNSUpdateError
1415 class DDNSProviderSelfhost(DDNSProtocolDynDNS2
, DDNSProvider
):
1416 handle
= "selfhost.de"
1417 name
= "Selfhost.de"
1418 website
= "http://www.selfhost.de/"
1419 protocols
= ("ipv4",)
1421 url
= "https://carol.selfhost.de/nic/update"
1423 def prepare_request_data(self
, proto
):
1424 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, proto
)
1432 class DDNSProviderSPDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
1433 handle
= "spdns.org"
1435 website
= "http://spdns.org/"
1437 # Detailed information about request and response codes are provided
1438 # by the vendor. They are using almost the same mechanism and status
1439 # codes as dyndns.org so we can inherit all those stuff.
1441 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1442 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1444 url
= "https://update.spdns.de/nic/update"
1448 return self
.get("username") or self
.hostname
1452 return self
.get("password") or self
.token
1455 class DDNSProviderStrato(DDNSProtocolDynDNS2
, DDNSProvider
):
1456 handle
= "strato.com"
1458 website
= "http:/www.strato.com/"
1459 protocols
= ("ipv4",)
1461 # Information about the request and response can be obtained here:
1462 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1464 url
= "https://dyndns.strato.com/nic/update"
1466 def prepare_request_data(self
, proto
):
1467 data
= DDNSProtocolDynDNS2
.prepare_request_data(self
, proto
)
1470 "backupmx" : "NOCHG"
1476 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
1477 handle
= "twodns.de"
1479 website
= "http://www.twodns.de"
1480 protocols
= ("ipv4",)
1482 # Detailed information about the request can be found here
1483 # http://twodns.de/en/faqs
1484 # http://twodns.de/en/api
1486 url
= "https://update.twodns.de/update"
1488 def prepare_request_data(self
, proto
):
1489 assert proto
== "ipv4"
1492 "ip" : self
.get_address(proto
),
1493 "hostname" : self
.hostname
1499 class DDNSProviderUdmedia(DDNSProtocolDynDNS2
, DDNSProvider
):
1500 handle
= "udmedia.de"
1501 name
= "Udmedia GmbH"
1502 website
= "http://www.udmedia.de"
1503 protocols
= ("ipv4",)
1505 # Information about the request can be found here
1506 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1508 url
= "https://www.udmedia.de/nic/update"
1511 class DDNSProviderVariomedia(DDNSProtocolDynDNS2
, DDNSProvider
):
1512 handle
= "variomedia.de"
1514 website
= "http://www.variomedia.de/"
1515 protocols
= ("ipv6", "ipv4",)
1517 # Detailed information about the request can be found here
1518 # https://dyndns.variomedia.de/
1520 url
= "https://dyndns.variomedia.de/nic/update"
1522 def prepare_request_data(self
, proto
):
1524 "hostname" : self
.hostname
,
1525 "myip" : self
.get_address(proto
),
1531 class DDNSProviderXLhost(DDNSProtocolDynDNS2
, DDNSProvider
):
1532 handle
= "xlhost.de"
1534 website
= "http://xlhost.de/"
1535 protocols
= ("ipv4",)
1537 # Information about the format of the HTTP request is to be found
1538 # here: https://xlhost.de/faq/index_html?topicId=CQA2ELIPO4SQ
1540 url
= "https://nsupdate.xlhost.de/"
1543 class DDNSProviderZoneedit(DDNSProvider
):
1544 handle
= "zoneedit.com"
1546 website
= "http://www.zoneedit.com"
1547 protocols
= ("ipv4",)
1549 # Detailed information about the request and the response codes can be
1551 # http://www.zoneedit.com/doc/api/other.html
1552 # http://www.zoneedit.com/faq.html
1554 url
= "https://dynamic.zoneedit.com/auth/dynamic.html"
1556 def update_protocol(self
, proto
):
1558 "dnsto" : self
.get_address(proto
),
1559 "host" : self
.hostname
1562 # Send update to the server.
1563 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
1566 # Get the full response message.
1567 output
= response
.read()
1569 # Handle success messages.
1570 if output
.startswith("<SUCCESS"):
1573 # Handle error codes.
1574 if output
.startswith("invalid login"):
1575 raise DDNSAuthenticationError
1576 elif output
.startswith("<ERROR CODE=\"704\""):
1577 raise DDNSRequestError(_("No valid FQDN was given."))
1578 elif output
.startswith("<ERROR CODE=\"702\""):
1579 raise DDNSRequestError(_("Too frequent update requests have been sent."))
1581 # If we got here, some other update error happened.
1582 raise DDNSUpdateError
1585 class DDNSProviderDNSmadeEasy(DDNSProvider
):
1586 handle
= "dnsmadeeasy.com"
1587 name
= "DNSmadeEasy.com"
1588 website
= "http://www.dnsmadeeasy.com/"
1589 protocols
= ("ipv4",)
1591 # DNS Made Easy Nameserver Provider also offering Dynamic DNS
1592 # Documentation can be found here:
1593 # http://www.dnsmadeeasy.com/dynamic-dns/
1595 url
= "https://cp.dnsmadeeasy.com/servlet/updateip?"
1596 can_remove_records
= False
1598 def update_protocol(self
, proto
):
1600 "ip" : self
.get_address(proto
),
1601 "id" : self
.hostname
,
1602 "username" : self
.username
,
1603 "password" : self
.password
,
1606 # Send update to the server.
1607 response
= self
.send_request(self
.url
, data
=data
)
1609 # Get the full response message.
1610 output
= response
.read()
1612 # Handle success messages.
1613 if output
.startswith("success") or output
.startswith("error-record-ip-same"):
1616 # Handle error codes.
1617 if output
.startswith("error-auth-suspend"):
1618 raise DDNSRequestError(_("Account has been suspended."))
1620 elif output
.startswith("error-auth-voided"):
1621 raise DDNSRequestError(_("Account has been revoked."))
1623 elif output
.startswith("error-record-invalid"):
1624 raise DDNSRequestError(_("Specified host does not exist."))
1626 elif output
.startswith("error-auth"):
1627 raise DDNSAuthenticationError
1629 # If we got here, some other update error happened.
1630 raise DDNSUpdateError(_("Server response: %s") % output
)
1633 class DDNSProviderZZZZ(DDNSProvider
):
1636 website
= "https://zzzz.io"
1637 protocols
= ("ipv6", "ipv4",)
1639 # Detailed information about the update request can be found here:
1640 # https://zzzz.io/faq/
1642 # Details about the possible response codes have been provided in the bugtracker:
1643 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1645 url
= "https://zzzz.io/api/v1/update"
1646 can_remove_records
= False
1648 def update_protocol(self
, proto
):
1650 "ip" : self
.get_address(proto
),
1651 "token" : self
.token
,
1655 data
["type"] = "aaaa"
1657 # zzzz uses the host from the full hostname as part
1658 # of the update url.
1659 host
, domain
= self
.hostname
.split(".", 1)
1661 # Add host value to the update url.
1662 url
= "%s/%s" % (self
.url
, host
)
1664 # Send update to the server.
1666 response
= self
.send_request(url
, data
=data
)
1668 # Handle error codes.
1669 except DDNSNotFound
:
1670 raise DDNSRequestError(_("Invalid hostname specified"))
1672 # Handle success messages.
1673 if response
.code
== 200:
1676 # If we got here, some other update error happened.
1677 raise DDNSUpdateError