]>
git.ipfire.org Git - 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 ###############################################################################
25 import xml
.dom
.minidom
29 # Import all possible exception types.
32 logger
= logging
.getLogger("ddns.providers")
39 Returns a dict with all automatically registered providers.
41 return _providers
.copy()
43 class DDNSProvider(object):
44 # A short string that uniquely identifies
48 # The full name of the provider.
51 # A weburl to the homepage of the provider.
52 # (Where to register a new account?)
55 # A list of supported protocols.
56 protocols
= ("ipv6", "ipv4")
60 # Automatically register all providers.
61 class __metaclass__(type):
62 def __init__(provider
, name
, bases
, dict):
63 type.__init
__(provider
, name
, bases
, dict)
65 # The main class from which is inherited is not registered
67 if name
== "DDNSProvider":
70 if not all((provider
.handle
, provider
.name
, provider
.website
)):
71 raise DDNSError(_("Provider is not properly configured"))
73 assert not _providers
.has_key(provider
.handle
), \
74 "Provider '%s' has already been registered" % provider
.handle
76 _providers
[provider
.handle
] = provider
78 def __init__(self
, core
, **settings
):
81 # Copy a set of default settings and
82 # update them by those from the configuration file.
83 self
.settings
= self
.DEFAULT_SETTINGS
.copy()
84 self
.settings
.update(settings
)
87 return "<DDNS Provider %s (%s)>" % (self
.name
, self
.handle
)
89 def __cmp__(self
, other
):
90 return cmp(self
.hostname
, other
.hostname
)
92 def get(self
, key
, default
=None):
94 Get a setting from the settings dictionary.
96 return self
.settings
.get(key
, default
)
101 Fast access to the hostname.
103 return self
.get("hostname")
108 Fast access to the username.
110 return self
.get("username")
115 Fast access to the password.
117 return self
.get("password")
122 Fast access to the token.
124 return self
.get("token")
126 def __call__(self
, force
=False):
128 logger
.debug(_("Updating %s forced") % self
.hostname
)
130 # Check if we actually need to update this host.
131 elif self
.is_uptodate(self
.protocols
):
132 logger
.debug(_("%s is already up to date") % self
.hostname
)
135 # Execute the update.
139 raise NotImplementedError
141 def is_uptodate(self
, protos
):
143 Returns True if this host is already up to date
144 and does not need to change the IP address on the
148 addresses
= self
.core
.system
.resolve(self
.hostname
, proto
)
150 current_address
= self
.get_address(proto
)
152 # If no addresses for the given protocol exist, we
154 if current_address
is None and not addresses
:
157 if not current_address
in addresses
:
162 def send_request(self
, *args
, **kwargs
):
164 Proxy connection to the send request
167 return self
.core
.system
.send_request(*args
, **kwargs
)
169 def get_address(self
, proto
, default
=None):
171 Proxy method to get the current IP address.
173 return self
.core
.system
.get_address(proto
) or default
176 class DDNSProtocolDynDNS2(object):
178 This is an abstract class that implements the DynDNS updater
179 protocol version 2. As this is a popular way to update dynamic
180 DNS records, this class is supposed make the provider classes
184 # Information about the format of the request is to be found
185 # http://dyn.com/support/developers/api/perform-update/
186 # http://dyn.com/support/developers/api/return-codes/
188 def _prepare_request_data(self
):
190 "hostname" : self
.hostname
,
191 "myip" : self
.get_address("ipv4"),
197 data
= self
._prepare
_request
_data
()
199 # Send update to the server.
200 response
= self
.send_request(self
.url
, data
=data
,
201 username
=self
.username
, password
=self
.password
)
203 # Get the full response message.
204 output
= response
.read()
206 # Handle success messages.
207 if output
.startswith("good") or output
.startswith("nochg"):
210 # Handle error codes.
211 if output
== "badauth":
212 raise DDNSAuthenticationError
213 elif output
== "aduse":
215 elif output
== "notfqdn":
216 raise DDNSRequestError(_("No valid FQDN was given."))
217 elif output
== "nohost":
218 raise DDNSRequestError(_("Specified host does not exist."))
219 elif output
== "911":
220 raise DDNSInternalServerError
221 elif output
== "dnserr":
222 raise DDNSInternalServerError(_("DNS error encountered."))
224 # If we got here, some other update error happened.
225 raise DDNSUpdateError(_("Server response: %s") % output
)
228 class DDNSProviderAllInkl(DDNSProvider
):
229 handle
= "all-inkl.com"
230 name
= "All-inkl.com"
231 website
= "http://all-inkl.com/"
232 protocols
= ("ipv4",)
234 # There are only information provided by the vendor how to
235 # perform an update on a FRITZ Box. Grab requried informations
237 # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
239 url
= "http://dyndns.kasserver.com"
242 # There is no additional data required so we directly can
244 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
)
246 # Get the full response message.
247 output
= response
.read()
249 # Handle success messages.
250 if output
.startswith("good") or output
.startswith("nochg"):
253 # If we got here, some other update error happened.
254 raise DDNSUpdateError
257 class DDNSProviderBindNsupdate(DDNSProvider
):
259 name
= "BIND nsupdate utility"
260 website
= "http://en.wikipedia.org/wiki/Nsupdate"
265 scriptlet
= self
.__make
_scriptlet
()
267 # -v enables TCP hence we transfer keys and other data that may
268 # exceed the size of one packet.
269 # -t sets the timeout
270 command
= ["nsupdate", "-v", "-t", "60"]
272 p
= subprocess
.Popen(command
, shell
=True,
273 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
275 stdout
, stderr
= p
.communicate(scriptlet
)
277 if p
.returncode
== 0:
280 raise DDNSError("nsupdate terminated with error code: %s\n %s" % (p
.returncode
, stderr
))
282 def __make_scriptlet(self
):
285 # Set a different server the update is sent to.
286 server
= self
.get("server", None)
288 scriptlet
.append("server %s" % server
)
290 key
= self
.get("key", None)
292 secret
= self
.get("secret")
294 scriptlet
.append("key %s %s" % (key
, secret
))
296 ttl
= self
.get("ttl", self
.DEFAULT_TTL
)
298 # Perform an update for each supported protocol.
299 for rrtype
, proto
in (("AAAA", "ipv6"), ("A", "ipv4")):
300 address
= self
.get_address(proto
)
304 scriptlet
.append("update delete %s. %s" % (self
.hostname
, rrtype
))
305 scriptlet
.append("update add %s. %s %s %s" % \
306 (self
.hostname
, ttl
, rrtype
, address
))
308 # Send the actions to the server.
309 scriptlet
.append("send")
310 scriptlet
.append("quit")
312 logger
.debug(_("Scriptlet:"))
313 for line
in scriptlet
:
314 # Masquerade the line with the secret key.
315 if line
.startswith("key"):
316 line
= "key **** ****"
318 logger
.debug(" %s" % line
)
320 return "\n".join(scriptlet
)
323 class DDNSProviderDHS(DDNSProvider
):
325 name
= "DHS International"
326 website
= "http://dhs.org/"
327 protocols
= ("ipv4",)
329 # No information about the used update api provided on webpage,
330 # grabed from source code of ez-ipudate.
332 url
= "http://members.dhs.org/nic/hosts"
336 "domain" : self
.hostname
,
337 "ip" : self
.get_address("ipv4"),
339 "hostcmdstage" : "2",
343 # Send update to the server.
344 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
347 # Handle success messages.
348 if response
.code
== 200:
351 # If we got here, some other update error happened.
352 raise DDNSUpdateError
355 class DDNSProviderDNSpark(DDNSProvider
):
356 handle
= "dnspark.com"
358 website
= "http://dnspark.com/"
359 protocols
= ("ipv4",)
361 # Informations to the used api can be found here:
362 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
364 url
= "https://control.dnspark.com/api/dynamic/update.php"
368 "domain" : self
.hostname
,
369 "ip" : self
.get_address("ipv4"),
372 # Send update to the server.
373 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
376 # Get the full response message.
377 output
= response
.read()
379 # Handle success messages.
380 if output
.startswith("ok") or output
.startswith("nochange"):
383 # Handle error codes.
384 if output
== "unauth":
385 raise DDNSAuthenticationError
386 elif output
== "abuse":
388 elif output
== "blocked":
389 raise DDNSBlockedError
390 elif output
== "nofqdn":
391 raise DDNSRequestError(_("No valid FQDN was given."))
392 elif output
== "nohost":
393 raise DDNSRequestError(_("Invalid hostname specified."))
394 elif output
== "notdyn":
395 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
396 elif output
== "invalid":
397 raise DDNSRequestError(_("Invalid IP address has been sent."))
399 # If we got here, some other update error happened.
400 raise DDNSUpdateError
403 class DDNSProviderDtDNS(DDNSProvider
):
406 website
= "http://dtdns.com/"
407 protocols
= ("ipv4",)
409 # Information about the format of the HTTPS request is to be found
410 # http://www.dtdns.com/dtsite/updatespec
412 url
= "https://www.dtdns.com/api/autodns.cfm"
416 "ip" : self
.get_address("ipv4"),
417 "id" : self
.hostname
,
421 # Send update to the server.
422 response
= self
.send_request(self
.url
, data
=data
)
424 # Get the full response message.
425 output
= response
.read()
427 # Remove all leading and trailing whitespace.
428 output
= output
.strip()
430 # Handle success messages.
431 if "now points to" in output
:
434 # Handle error codes.
435 if output
== "No hostname to update was supplied.":
436 raise DDNSRequestError(_("No hostname specified."))
438 elif output
== "The hostname you supplied is not valid.":
439 raise DDNSRequestError(_("Invalid hostname specified."))
441 elif output
== "The password you supplied is not valid.":
442 raise DDNSAuthenticationError
444 elif output
== "Administration has disabled this account.":
445 raise DDNSRequestError(_("Account has been disabled."))
447 elif output
== "Illegal character in IP.":
448 raise DDNSRequestError(_("Invalid IP address has been sent."))
450 elif output
== "Too many failed requests.":
451 raise DDNSRequestError(_("Too many failed requests."))
453 # If we got here, some other update error happened.
454 raise DDNSUpdateError
457 class DDNSProviderDynDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
458 handle
= "dyndns.org"
460 website
= "http://dyn.com/dns/"
461 protocols
= ("ipv4",)
463 # Information about the format of the request is to be found
464 # http://http://dyn.com/support/developers/api/perform-update/
465 # http://dyn.com/support/developers/api/return-codes/
467 url
= "https://members.dyndns.org/nic/update"
470 class DDNSProviderDynU(DDNSProtocolDynDNS2
, DDNSProvider
):
473 website
= "http://dynu.com/"
474 protocols
= ("ipv6", "ipv4",)
476 # Detailed information about the request and response codes
477 # are available on the providers webpage.
478 # http://dynu.com/Default.aspx?page=dnsapi
480 url
= "https://api.dynu.com/nic/update"
482 def _prepare_request_data(self
):
483 data
= DDNSProviderDynDNS
._prepare
_request
_data
(self
)
485 # This one supports IPv6
487 "myipv6" : self
.get_address("ipv6"),
493 class DDNSProviderEasyDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
494 handle
= "easydns.com"
496 website
= "http://www.easydns.com/"
497 protocols
= ("ipv4",)
499 # There is only some basic documentation provided by the vendor,
500 # also searching the web gain very poor results.
501 # http://mediawiki.easydns.com/index.php/Dynamic_DNS
503 url
= "http://api.cp.easydns.com/dyn/tomato.php"
506 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider
):
507 handle
= "freedns.afraid.org"
508 name
= "freedns.afraid.org"
509 website
= "http://freedns.afraid.org/"
511 # No information about the request or response could be found on the vendor
512 # page. All used values have been collected by testing.
513 url
= "https://freedns.afraid.org/dynamic/update.php"
517 return self
.get("proto")
520 address
= self
.get_address(self
.proto
)
526 # Add auth token to the update url.
527 url
= "%s?%s" % (self
.url
, self
.token
)
529 # Send update to the server.
530 response
= self
.send_request(url
, data
=data
)
532 # Get the full response message.
533 output
= response
.read()
535 # Handle success messages.
536 if output
.startswith("Updated") or "has not changed" in output
:
539 # Handle error codes.
540 if output
== "ERROR: Unable to locate this record":
541 raise DDNSAuthenticationError
542 elif "is an invalid IP address" in output
:
543 raise DDNSRequestError(_("Invalid IP address has been sent."))
545 # If we got here, some other update error happened.
546 raise DDNSUpdateError
549 class DDNSProviderLightningWireLabs(DDNSProvider
):
550 handle
= "dns.lightningwirelabs.com"
551 name
= "Lightning Wire Labs DNS Service"
552 website
= "http://dns.lightningwirelabs.com/"
554 # Information about the format of the HTTPS request is to be found
555 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
557 url
= "https://dns.lightningwirelabs.com/update"
561 "hostname" : self
.hostname
,
562 "address6" : self
.get_address("ipv6", "-"),
563 "address4" : self
.get_address("ipv4", "-"),
566 # Check if a token has been set.
568 data
["token"] = self
.token
570 # Check for username and password.
571 elif self
.username
and self
.password
:
573 "username" : self
.username
,
574 "password" : self
.password
,
577 # Raise an error if no auth details are given.
579 raise DDNSConfigurationError
581 # Send update to the server.
582 response
= self
.send_request(self
.url
, data
=data
)
584 # Handle success messages.
585 if response
.code
== 200:
588 # If we got here, some other update error happened.
589 raise DDNSUpdateError
592 class DDNSProviderNamecheap(DDNSProvider
):
593 handle
= "namecheap.com"
595 website
= "http://namecheap.com"
596 protocols
= ("ipv4",)
598 # Information about the format of the HTTP request is to be found
599 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
600 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
602 url
= "https://dynamicdns.park-your-domain.com/update"
604 def parse_xml(self
, document
, content
):
605 # Send input to the parser.
606 xmldoc
= xml
.dom
.minidom
.parseString(document
)
608 # Get XML elements by the given content.
609 element
= xmldoc
.getElementsByTagName(content
)
611 # If no element has been found, we directly can return None.
615 # Only get the first child from an element, even there are more than one.
616 firstchild
= element
[0].firstChild
618 # Get the value of the child.
619 value
= firstchild
.nodeValue
625 # Namecheap requires the hostname splitted into a host and domain part.
626 host
, domain
= self
.hostname
.split(".", 1)
629 "ip" : self
.get_address("ipv4"),
630 "password" : self
.password
,
635 # Send update to the server.
636 response
= self
.send_request(self
.url
, data
=data
)
638 # Get the full response message.
639 output
= response
.read()
641 # Handle success messages.
642 if self
.parse_xml(output
, "IP") == self
.get_address("ipv4"):
645 # Handle error codes.
646 errorcode
= self
.parse_xml(output
, "ResponseNumber")
648 if errorcode
== "304156":
649 raise DDNSAuthenticationError
650 elif errorcode
== "316153":
651 raise DDNSRequestError(_("Domain not found."))
652 elif errorcode
== "316154":
653 raise DDNSRequestError(_("Domain not active."))
654 elif errorcode
in ("380098", "380099"):
655 raise DDNSInternalServerError
657 # If we got here, some other update error happened.
658 raise DDNSUpdateError
661 class DDNSProviderNOIP(DDNSProtocolDynDNS2
, DDNSProvider
):
664 website
= "http://www.no-ip.com/"
665 protocols
= ("ipv4",)
667 # Information about the format of the HTTP request is to be found
668 # here: http://www.no-ip.com/integrate/request and
669 # here: http://www.no-ip.com/integrate/response
671 url
= "http://dynupdate.no-ip.com/nic/update"
673 def _prepare_request_data(self
):
675 "hostname" : self
.hostname
,
676 "address" : self
.get_address("ipv4"),
682 class DDNSProviderOVH(DDNSProtocolDynDNS2
, DDNSProvider
):
685 website
= "http://www.ovh.com/"
686 protocols
= ("ipv4",)
688 # OVH only provides very limited information about how to
689 # update a DynDNS host. They only provide the update url
690 # on the their german subpage.
692 # http://hilfe.ovh.de/DomainDynHost
694 url
= "https://www.ovh.com/nic/update"
696 def _prepare_request_data(self
):
697 data
= DDNSProtocolDynDNS2
._prepare_request_data(self
)
705 class DDNSProviderRegfish(DDNSProvider
):
706 handle
= "regfish.com"
707 name
= "Regfish GmbH"
708 website
= "http://www.regfish.com/"
710 # A full documentation to the providers api can be found here
711 # but is only available in german.
712 # https://www.regfish.de/domains/dyndns/dokumentation
714 url
= "https://dyndns.regfish.de/"
718 "fqdn" : self
.hostname
,
721 # Check if we update an IPv6 address.
722 address6
= self
.get_address("ipv6")
724 data
["ipv6"] = address6
726 # Check if we update an IPv4 address.
727 address4
= self
.get_address("ipv4")
729 data
["ipv4"] = address4
731 # Raise an error if none address is given.
732 if not data
.has_key("ipv6") and not data
.has_key("ipv4"):
733 raise DDNSConfigurationError
735 # Check if a token has been set.
737 data
["token"] = self
.token
739 # Raise an error if no token and no useranem and password
741 elif not self
.username
and not self
.password
:
742 raise DDNSConfigurationError(_("No Auth details specified."))
744 # HTTP Basic Auth is only allowed if no token is used.
746 # Send update to the server.
747 response
= self
.send_request(self
.url
, data
=data
)
749 # Send update to the server.
750 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
753 # Get the full response message.
754 output
= response
.read()
756 # Handle success messages.
757 if "100" in output
or "101" in output
:
760 # Handle error codes.
761 if "401" or "402" in output
:
762 raise DDNSAuthenticationError
763 elif "408" in output
:
764 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
765 elif "409" in output
:
766 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
767 elif "412" in output
:
768 raise DDNSRequestError(_("No valid FQDN was given."))
769 elif "414" in output
:
770 raise DDNSInternalServerError
772 # If we got here, some other update error happened.
773 raise DDNSUpdateError
776 class DDNSProviderSelfhost(DDNSProtocolDynDNS2
, DDNSProvider
):
777 handle
= "selfhost.de"
779 website
= "http://www.selfhost.de/"
780 protocols
= ("ipv4",)
782 url
= "https://carol.selfhost.de/nic/update"
784 def _prepare_request_data(self
):
785 data
= DDNSProviderDynDNS
._prepare
_request
_data
(self
)
793 class DDNSProviderSPDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
796 website
= "http://spdns.org/"
797 protocols
= ("ipv4",)
799 # Detailed information about request and response codes are provided
800 # by the vendor. They are using almost the same mechanism and status
801 # codes as dyndns.org so we can inherit all those stuff.
803 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
804 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
806 url
= "https://update.spdns.de/nic/update"
809 class DDNSProviderStrato(DDNSProtocolDynDNS2
, DDNSProvider
):
810 handle
= "strato.com"
812 website
= "http:/www.strato.com/"
813 protocols
= ("ipv4",)
815 # Information about the request and response can be obtained here:
816 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
818 url
= "https://dyndns.strato.com/nic/update"
821 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2
, DDNSProvider
):
824 website
= "http://www.twodns.de"
825 protocols
= ("ipv4",)
827 # Detailed information about the request can be found here
828 # http://twodns.de/en/faqs
829 # http://twodns.de/en/api
831 url
= "https://update.twodns.de/update"
833 def _prepare_request_data(self
):
835 "ip" : self
.get_address("ipv4"),
836 "hostname" : self
.hostname
842 class DDNSProviderUdmedia(DDNSProtocolDynDNS2
, DDNSProvider
):
843 handle
= "udmedia.de"
844 name
= "Udmedia GmbH"
845 website
= "http://www.udmedia.de"
846 protocols
= ("ipv4",)
848 # Information about the request can be found here
849 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
851 url
= "https://www.udmedia.de/nic/update"
854 class DDNSProviderVariomedia(DDNSProtocolDynDNS2
, DDNSProvider
):
855 handle
= "variomedia.de"
857 website
= "http://www.variomedia.de/"
858 protocols
= ("ipv6", "ipv4",)
860 # Detailed information about the request can be found here
861 # https://dyndns.variomedia.de/
863 url
= "https://dyndns.variomedia.de/nic/update"
867 return self
.get("proto")
869 def _prepare_request_data(self
):
871 "hostname" : self
.hostname
,
872 "myip" : self
.get_address(self
.proto
)
878 class DDNSProviderZoneedit(DDNSProtocolDynDNS2
, DDNSProvider
):
879 handle
= "zoneedit.com"
881 website
= "http://www.zoneedit.com"
882 protocols
= ("ipv4",)
884 # Detailed information about the request and the response codes can be
886 # http://www.zoneedit.com/doc/api/other.html
887 # http://www.zoneedit.com/faq.html
889 url
= "https://dynamic.zoneedit.com/auth/dynamic.html"
893 return self
.get("proto")
897 "dnsto" : self
.get_address(self
.proto
),
898 "host" : self
.hostname
901 # Send update to the server.
902 response
= self
.send_request(self
.url
, username
=self
.username
, password
=self
.password
,
905 # Get the full response message.
906 output
= response
.read()
908 # Handle success messages.
909 if output
.startswith("<SUCCESS"):
912 # Handle error codes.
913 if output
.startswith("invalid login"):
914 raise DDNSAuthenticationError
915 elif output
.startswith("<ERROR CODE=\"704\""):
916 raise DDNSRequestError(_("No valid FQDN was given."))
917 elif output
.startswith("<ERROR CODE=\"702\""):
918 raise DDNSInternalServerError
920 # If we got here, some other update error happened.
921 raise DDNSUpdateError