]> git.ipfire.org Git - oddments/ddns.git/blob - src/ddns/providers.py
Soft-fail on network errors
[oddments/ddns.git] / src / ddns / providers.py
1 #!/usr/bin/python
2 ###############################################################################
3 # #
4 # ddns - A dynamic DNS client for IPFire #
5 # Copyright (C) 2012 IPFire development team #
6 # #
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. #
11 # #
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. #
16 # #
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/>. #
19 # #
20 ###############################################################################
21
22 import datetime
23 import logging
24 import os
25 import subprocess
26 import urllib2
27 import xml.dom.minidom
28
29 from i18n import _
30
31 # Import all possible exception types.
32 from .errors import *
33
34 logger = logging.getLogger("ddns.providers")
35 logger.propagate = 1
36
37 _providers = {}
38
39 def get():
40 """
41 Returns a dict with all automatically registered providers.
42 """
43 return _providers.copy()
44
45 class DDNSProvider(object):
46 # A short string that uniquely identifies
47 # this provider.
48 handle = None
49
50 # The full name of the provider.
51 name = None
52
53 # A weburl to the homepage of the provider.
54 # (Where to register a new account?)
55 website = None
56
57 # A list of supported protocols.
58 protocols = ("ipv6", "ipv4")
59
60 DEFAULT_SETTINGS = {}
61
62 # holdoff time - Number of days no update is performed unless
63 # the IP address has changed.
64 holdoff_days = 30
65
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
69
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
73
74 # Automatically register all providers.
75 class __metaclass__(type):
76 def __init__(provider, name, bases, dict):
77 type.__init__(provider, name, bases, dict)
78
79 # The main class from which is inherited is not registered
80 # as a provider.
81 if name == "DDNSProvider":
82 return
83
84 if not all((provider.handle, provider.name, provider.website)):
85 raise DDNSError(_("Provider is not properly configured"))
86
87 assert not _providers.has_key(provider.handle), \
88 "Provider '%s' has already been registered" % provider.handle
89
90 _providers[provider.handle] = provider
91
92 @staticmethod
93 def supported():
94 """
95 Should be overwritten to check if the system the code is running
96 on has all the required tools to support this provider.
97 """
98 return True
99
100 def __init__(self, core, **settings):
101 self.core = core
102
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)
107
108 def __repr__(self):
109 return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
110
111 def __cmp__(self, other):
112 return cmp(self.hostname, other.hostname)
113
114 @property
115 def db(self):
116 return self.core.db
117
118 def get(self, key, default=None):
119 """
120 Get a setting from the settings dictionary.
121 """
122 return self.settings.get(key, default)
123
124 @property
125 def hostname(self):
126 """
127 Fast access to the hostname.
128 """
129 return self.get("hostname")
130
131 @property
132 def username(self):
133 """
134 Fast access to the username.
135 """
136 return self.get("username")
137
138 @property
139 def password(self):
140 """
141 Fast access to the password.
142 """
143 return self.get("password")
144
145 @property
146 def token(self):
147 """
148 Fast access to the token.
149 """
150 return self.get("token")
151
152 def __call__(self, force=False):
153 if force:
154 logger.debug(_("Updating %s forced") % self.hostname)
155
156 # Do nothing if the last update has failed or no update is required
157 elif self.has_failure or not self.requires_update:
158 return
159
160 # Execute the update.
161 try:
162 self.update()
163
164 # 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 except DDNSNetworkError as e:
168 raise
169
170 # In case of any errors, log the failed request and
171 # raise the exception.
172 except DDNSError as e:
173 self.core.db.log_failure(self.hostname, e)
174 raise
175
176 logger.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \
177 { "hostname" : self.hostname, "provider" : self.name })
178 self.core.db.log_success(self.hostname)
179
180 def update(self):
181 for protocol in self.protocols:
182 if self.have_address(protocol):
183 self.update_protocol(protocol)
184 elif self.can_remove_records:
185 self.remove_protocol(protocol)
186
187 def update_protocol(self, proto):
188 raise NotImplementedError
189
190 def remove_protocol(self, proto):
191 if not self.can_remove_records:
192 raise RuntimeError, "can_remove_records is enabled, but remove_protocol() not implemented"
193
194 raise NotImplementedError
195
196 @property
197 def requires_update(self):
198 # If the IP addresses have changed, an update is required
199 if self.ip_address_changed(self.protocols):
200 logger.debug(_("An update for %(hostname)s (%(provider)s)"
201 " is performed because of an IP address change") % \
202 { "hostname" : self.hostname, "provider" : self.name })
203
204 return True
205
206 # If the holdoff time has expired, an update is required, too
207 if self.holdoff_time_expired():
208 logger.debug(_("An update for %(hostname)s (%(provider)s)"
209 " is performed because the holdoff time has expired") % \
210 { "hostname" : self.hostname, "provider" : self.name })
211
212 return True
213
214 # Otherwise, we don't need to perform an update
215 logger.debug(_("No update required for %(hostname)s (%(provider)s)") % \
216 { "hostname" : self.hostname, "provider" : self.name })
217
218 return False
219
220 @property
221 def has_failure(self):
222 """
223 Returns True when the last update has failed and no retry
224 should be performed, yet.
225 """
226 last_status = self.db.last_update_status(self.hostname)
227
228 # Return False if the last update has not failed.
229 if not last_status == "failure":
230 return False
231
232 # If there is no holdoff time, we won't update ever again.
233 if self.holdoff_failure_days is None:
234 logger.warning(_("An update has not been performed because earlier updates failed for %s") \
235 % self.hostname)
236 logger.warning(_("There will be no retries"))
237
238 return True
239
240 # Determine when the holdoff time ends
241 last_update = self.db.last_update(self.hostname, status=last_status)
242 holdoff_end = last_update + datetime.timedelta(days=self.holdoff_failure_days)
243
244 now = datetime.datetime.utcnow()
245 if now < holdoff_end:
246 failure_message = self.db.last_update_failure_message(self.hostname)
247
248 logger.warning(_("An update has not been performed because earlier updates failed for %s") \
249 % self.hostname)
250
251 if failure_message:
252 logger.warning(_("Last failure message:"))
253
254 for line in failure_message.splitlines():
255 logger.warning(" %s" % line)
256
257 logger.warning(_("Further updates will be withheld until %s") % holdoff_end)
258
259 return True
260
261 return False
262
263 def ip_address_changed(self, protos):
264 """
265 Returns True if this host is already up to date
266 and does not need to change the IP address on the
267 name server.
268 """
269 for proto in protos:
270 addresses = self.core.system.resolve(self.hostname, proto)
271 current_address = self.get_address(proto)
272
273 # Handle if the system has not got any IP address from a protocol
274 # (i.e. had full dual-stack connectivity which it has not any more)
275 if current_address is None:
276 # If addresses still exists in the DNS system and if this provider
277 # is able to remove records, we will do that.
278 if addresses and self.can_remove_records:
279 return True
280
281 # Otherwise, we cannot go on...
282 continue
283
284 if not current_address in addresses:
285 return True
286
287 return False
288
289 def holdoff_time_expired(self):
290 """
291 Returns true if the holdoff time has expired
292 and the host requires an update
293 """
294 # If no holdoff days is defined, we cannot go on
295 if not self.holdoff_days:
296 return False
297
298 # Get the timestamp of the last successfull update
299 last_update = self.db.last_update(self.hostname, status="success")
300
301 # If no timestamp has been recorded, no update has been
302 # performed. An update should be performed now.
303 if not last_update:
304 return True
305
306 # Determine when the holdoff time ends
307 holdoff_end = last_update + datetime.timedelta(days=self.holdoff_days)
308
309 now = datetime.datetime.utcnow()
310
311 if now >= holdoff_end:
312 logger.debug("The holdoff time has expired for %s" % self.hostname)
313 return True
314 else:
315 logger.debug("Updates for %s are held off until %s" % \
316 (self.hostname, holdoff_end))
317 return False
318
319 def send_request(self, *args, **kwargs):
320 """
321 Proxy connection to the send request
322 method.
323 """
324 return self.core.system.send_request(*args, **kwargs)
325
326 def get_address(self, proto, default=None):
327 """
328 Proxy method to get the current IP address.
329 """
330 return self.core.system.get_address(proto) or default
331
332 def have_address(self, proto):
333 """
334 Returns True if an IP address for the given protocol
335 is known and usable.
336 """
337 address = self.get_address(proto)
338
339 if address:
340 return True
341
342 return False
343
344
345 class DDNSProtocolDynDNS2(object):
346 """
347 This is an abstract class that implements the DynDNS updater
348 protocol version 2. As this is a popular way to update dynamic
349 DNS records, this class is supposed make the provider classes
350 shorter and simpler.
351 """
352
353 # Information about the format of the request is to be found
354 # http://dyn.com/support/developers/api/perform-update/
355 # http://dyn.com/support/developers/api/return-codes/
356
357 # The DynDNS protocol version 2 does not allow to remove records
358 can_remove_records = False
359
360 def prepare_request_data(self, proto):
361 data = {
362 "hostname" : self.hostname,
363 "myip" : self.get_address(proto),
364 }
365
366 return data
367
368 def update_protocol(self, proto):
369 data = self.prepare_request_data(proto)
370
371 return self.send_request(data)
372
373 def send_request(self, data):
374 # Send update to the server.
375 response = DDNSProvider.send_request(self, self.url, data=data,
376 username=self.username, password=self.password)
377
378 # Get the full response message.
379 output = response.read()
380
381 # Handle success messages.
382 if output.startswith("good") or output.startswith("nochg"):
383 return
384
385 # Handle error codes.
386 if output == "badauth":
387 raise DDNSAuthenticationError
388 elif output == "abuse":
389 raise DDNSAbuseError
390 elif output == "notfqdn":
391 raise DDNSRequestError(_("No valid FQDN was given."))
392 elif output == "nohost":
393 raise DDNSRequestError(_("Specified host does not exist."))
394 elif output == "911":
395 raise DDNSInternalServerError
396 elif output == "dnserr":
397 raise DDNSInternalServerError(_("DNS error encountered."))
398 elif output == "badagent":
399 raise DDNSBlockedError
400
401 # If we got here, some other update error happened.
402 raise DDNSUpdateError(_("Server response: %s") % output)
403
404
405 class DDNSResponseParserXML(object):
406 """
407 This class provides a parser for XML responses which
408 will be sent by various providers. This class uses the python
409 shipped XML minidom module to walk through the XML tree and return
410 a requested element.
411 """
412
413 def get_xml_tag_value(self, document, content):
414 # Send input to the parser.
415 xmldoc = xml.dom.minidom.parseString(document)
416
417 # Get XML elements by the given content.
418 element = xmldoc.getElementsByTagName(content)
419
420 # If no element has been found, we directly can return None.
421 if not element:
422 return None
423
424 # Only get the first child from an element, even there are more than one.
425 firstchild = element[0].firstChild
426
427 # Get the value of the child.
428 value = firstchild.nodeValue
429
430 # Return the value.
431 return value
432
433
434 class DDNSProviderAllInkl(DDNSProvider):
435 handle = "all-inkl.com"
436 name = "All-inkl.com"
437 website = "http://all-inkl.com/"
438 protocols = ("ipv4",)
439
440 # There are only information provided by the vendor how to
441 # perform an update on a FRITZ Box. Grab requried informations
442 # from the net.
443 # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
444
445 url = "http://dyndns.kasserver.com"
446 can_remove_records = False
447
448 def update(self):
449 # There is no additional data required so we directly can
450 # send our request.
451 response = self.send_request(self.url, username=self.username, password=self.password)
452
453 # Get the full response message.
454 output = response.read()
455
456 # Handle success messages.
457 if output.startswith("good") or output.startswith("nochg"):
458 return
459
460 # If we got here, some other update error happened.
461 raise DDNSUpdateError
462
463
464 class DDNSProviderBindNsupdate(DDNSProvider):
465 handle = "nsupdate"
466 name = "BIND nsupdate utility"
467 website = "http://en.wikipedia.org/wiki/Nsupdate"
468
469 DEFAULT_TTL = 60
470
471 @staticmethod
472 def supported():
473 # Search if the nsupdate utility is available
474 paths = os.environ.get("PATH")
475
476 for path in paths.split(":"):
477 executable = os.path.join(path, "nsupdate")
478
479 if os.path.exists(executable):
480 return True
481
482 return False
483
484 def update(self):
485 scriptlet = self.__make_scriptlet()
486
487 # -v enables TCP hence we transfer keys and other data that may
488 # exceed the size of one packet.
489 # -t sets the timeout
490 command = ["nsupdate", "-v", "-t", "60"]
491
492 p = subprocess.Popen(command, shell=True,
493 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
494 )
495 stdout, stderr = p.communicate(scriptlet)
496
497 if p.returncode == 0:
498 return
499
500 raise DDNSError("nsupdate terminated with error code: %s\n %s" % (p.returncode, stderr))
501
502 def __make_scriptlet(self):
503 scriptlet = []
504
505 # Set a different server the update is sent to.
506 server = self.get("server", None)
507 if server:
508 scriptlet.append("server %s" % server)
509
510 # Set the DNS zone the host should be added to.
511 zone = self.get("zone", None)
512 if zone:
513 scriptlet.append("zone %s" % zone)
514
515 key = self.get("key", None)
516 if key:
517 secret = self.get("secret")
518
519 scriptlet.append("key %s %s" % (key, secret))
520
521 ttl = self.get("ttl", self.DEFAULT_TTL)
522
523 # Perform an update for each supported protocol.
524 for rrtype, proto in (("AAAA", "ipv6"), ("A", "ipv4")):
525 address = self.get_address(proto)
526 if not address:
527 continue
528
529 scriptlet.append("update delete %s. %s" % (self.hostname, rrtype))
530 scriptlet.append("update add %s. %s %s %s" % \
531 (self.hostname, ttl, rrtype, address))
532
533 # Send the actions to the server.
534 scriptlet.append("send")
535 scriptlet.append("quit")
536
537 logger.debug(_("Scriptlet:"))
538 for line in scriptlet:
539 # Masquerade the line with the secret key.
540 if line.startswith("key"):
541 line = "key **** ****"
542
543 logger.debug(" %s" % line)
544
545 return "\n".join(scriptlet)
546
547
548 class DDNSProviderChangeIP(DDNSProvider):
549 handle = "changeip.com"
550 name = "ChangeIP.com"
551 website = "https://changeip.com"
552 protocols = ("ipv4",)
553
554 # Detailed information about the update api can be found here.
555 # http://www.changeip.com/accounts/knowledgebase.php?action=displayarticle&id=34
556
557 url = "https://nic.changeip.com/nic/update"
558 can_remove_records = False
559
560 def update_protocol(self, proto):
561 data = {
562 "hostname" : self.hostname,
563 "myip" : self.get_address(proto),
564 }
565
566 # Send update to the server.
567 try:
568 response = self.send_request(self.url, username=self.username, password=self.password,
569 data=data)
570
571 # Handle error codes.
572 except urllib2.HTTPError, e:
573 if e.code == 422:
574 raise DDNSRequestError(_("Domain not found."))
575
576 raise
577
578 # Handle success message.
579 if response.code == 200:
580 return
581
582 # If we got here, some other update error happened.
583 raise DDNSUpdateError(_("Server response: %s") % output)
584
585
586 class DDNSProviderDDNSS(DDNSProvider):
587 handle = "ddnss.de"
588 name = "DDNSS"
589 website = "http://www.ddnss.de"
590 protocols = ("ipv4",)
591
592 # Detailed information about how to send the update request and possible response
593 # codes can be obtained from here.
594 # http://www.ddnss.de/info.php
595 # http://www.megacomputing.de/2014/08/dyndns-service-response-time/#more-919
596
597 url = "http://www.ddnss.de/upd.php"
598 can_remove_records = False
599
600 def update_protocol(self, proto):
601 data = {
602 "ip" : self.get_address(proto),
603 "host" : self.hostname,
604 }
605
606 # Check if a token has been set.
607 if self.token:
608 data["key"] = self.token
609
610 # Check if username and hostname are given.
611 elif self.username and self.password:
612 data.update({
613 "user" : self.username,
614 "pwd" : self.password,
615 })
616
617 # Raise an error if no auth details are given.
618 else:
619 raise DDNSConfigurationError
620
621 # Send update to the server.
622 response = self.send_request(self.url, data=data)
623
624 # This provider sends the response code as part of the header.
625 header = response.info()
626
627 # Get status information from the header.
628 output = header.getheader('ddnss-response')
629
630 # Handle success messages.
631 if output == "good" or output == "nochg":
632 return
633
634 # Handle error codes.
635 if output == "badauth":
636 raise DDNSAuthenticationError
637 elif output == "notfqdn":
638 raise DDNSRequestError(_("No valid FQDN was given."))
639 elif output == "nohost":
640 raise DDNSRequestError(_("Specified host does not exist."))
641 elif output == "911":
642 raise DDNSInternalServerError
643 elif output == "dnserr":
644 raise DDNSInternalServerError(_("DNS error encountered."))
645 elif output == "disabled":
646 raise DDNSRequestError(_("Account disabled or locked."))
647
648 # If we got here, some other update error happened.
649 raise DDNSUpdateError
650
651
652 class DDNSProviderDHS(DDNSProvider):
653 handle = "dhs.org"
654 name = "DHS International"
655 website = "http://dhs.org/"
656 protocols = ("ipv4",)
657
658 # No information about the used update api provided on webpage,
659 # grabed from source code of ez-ipudate.
660
661 url = "http://members.dhs.org/nic/hosts"
662 can_remove_records = False
663
664 def update_protocol(self, proto):
665 data = {
666 "domain" : self.hostname,
667 "ip" : self.get_address(proto),
668 "hostcmd" : "edit",
669 "hostcmdstage" : "2",
670 "type" : "4",
671 }
672
673 # Send update to the server.
674 response = self.send_request(self.url, username=self.username, password=self.password,
675 data=data)
676
677 # Handle success messages.
678 if response.code == 200:
679 return
680
681 # If we got here, some other update error happened.
682 raise DDNSUpdateError
683
684
685 class DDNSProviderDNSpark(DDNSProvider):
686 handle = "dnspark.com"
687 name = "DNS Park"
688 website = "http://dnspark.com/"
689 protocols = ("ipv4",)
690
691 # Informations to the used api can be found here:
692 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
693
694 url = "https://control.dnspark.com/api/dynamic/update.php"
695 can_remove_records = False
696
697 def update_protocol(self, proto):
698 data = {
699 "domain" : self.hostname,
700 "ip" : self.get_address(proto),
701 }
702
703 # Send update to the server.
704 response = self.send_request(self.url, username=self.username, password=self.password,
705 data=data)
706
707 # Get the full response message.
708 output = response.read()
709
710 # Handle success messages.
711 if output.startswith("ok") or output.startswith("nochange"):
712 return
713
714 # Handle error codes.
715 if output == "unauth":
716 raise DDNSAuthenticationError
717 elif output == "abuse":
718 raise DDNSAbuseError
719 elif output == "blocked":
720 raise DDNSBlockedError
721 elif output == "nofqdn":
722 raise DDNSRequestError(_("No valid FQDN was given."))
723 elif output == "nohost":
724 raise DDNSRequestError(_("Invalid hostname specified."))
725 elif output == "notdyn":
726 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
727 elif output == "invalid":
728 raise DDNSRequestError(_("Invalid IP address has been sent."))
729
730 # If we got here, some other update error happened.
731 raise DDNSUpdateError
732
733
734 class DDNSProviderDtDNS(DDNSProvider):
735 handle = "dtdns.com"
736 name = "DtDNS"
737 website = "http://dtdns.com/"
738 protocols = ("ipv4",)
739
740 # Information about the format of the HTTPS request is to be found
741 # http://www.dtdns.com/dtsite/updatespec
742
743 url = "https://www.dtdns.com/api/autodns.cfm"
744 can_remove_records = False
745
746 def update_protocol(self, proto):
747 data = {
748 "ip" : self.get_address(proto),
749 "id" : self.hostname,
750 "pw" : self.password
751 }
752
753 # Send update to the server.
754 response = self.send_request(self.url, data=data)
755
756 # Get the full response message.
757 output = response.read()
758
759 # Remove all leading and trailing whitespace.
760 output = output.strip()
761
762 # Handle success messages.
763 if "now points to" in output:
764 return
765
766 # Handle error codes.
767 if output == "No hostname to update was supplied.":
768 raise DDNSRequestError(_("No hostname specified."))
769
770 elif output == "The hostname you supplied is not valid.":
771 raise DDNSRequestError(_("Invalid hostname specified."))
772
773 elif output == "The password you supplied is not valid.":
774 raise DDNSAuthenticationError
775
776 elif output == "Administration has disabled this account.":
777 raise DDNSRequestError(_("Account has been disabled."))
778
779 elif output == "Illegal character in IP.":
780 raise DDNSRequestError(_("Invalid IP address has been sent."))
781
782 elif output == "Too many failed requests.":
783 raise DDNSRequestError(_("Too many failed requests."))
784
785 # If we got here, some other update error happened.
786 raise DDNSUpdateError
787
788
789 class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
790 handle = "dyndns.org"
791 name = "Dyn"
792 website = "http://dyn.com/dns/"
793 protocols = ("ipv4",)
794
795 # Information about the format of the request is to be found
796 # http://http://dyn.com/support/developers/api/perform-update/
797 # http://dyn.com/support/developers/api/return-codes/
798
799 url = "https://members.dyndns.org/nic/update"
800
801
802 class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
803 handle = "dynu.com"
804 name = "Dynu"
805 website = "http://dynu.com/"
806 protocols = ("ipv6", "ipv4",)
807
808 # Detailed information about the request and response codes
809 # are available on the providers webpage.
810 # http://dynu.com/Default.aspx?page=dnsapi
811
812 url = "https://api.dynu.com/nic/update"
813
814 # DynU sends the IPv6 and IPv4 address in one request
815
816 def update(self):
817 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
818
819 # This one supports IPv6
820 myipv6 = self.get_address("ipv6")
821
822 # Add update information if we have an IPv6 address.
823 if myipv6:
824 data["myipv6"] = myipv6
825
826 self.send_request(data)
827
828
829 class DDNSProviderEasyDNS(DDNSProvider):
830 handle = "easydns.com"
831 name = "EasyDNS"
832 website = "http://www.easydns.com/"
833 protocols = ("ipv4",)
834
835 # Detailed information about the request and response codes
836 # (API 1.3) are available on the providers webpage.
837 # https://fusion.easydns.com/index.php?/Knowledgebase/Article/View/102/7/dynamic-dns
838
839 url = "http://api.cp.easydns.com/dyn/tomato.php"
840
841 def update_protocol(self, proto):
842 data = {
843 "myip" : self.get_address(proto, "-"),
844 "hostname" : self.hostname,
845 }
846
847 # Send update to the server.
848 response = self.send_request(self.url, data=data,
849 username=self.username, password=self.password)
850
851 # Get the full response message.
852 output = response.read()
853
854 # Remove all leading and trailing whitespace.
855 output = output.strip()
856
857 # Handle success messages.
858 if output.startswith("NOERROR"):
859 return
860
861 # Handle error codes.
862 if output.startswith("NOACCESS"):
863 raise DDNSAuthenticationError
864
865 elif output.startswith("NOSERVICE"):
866 raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain."))
867
868 elif output.startswith("ILLEGAL INPUT"):
869 raise DDNSRequestError(_("Invalid data has been sent."))
870
871 elif output.startswith("TOOSOON"):
872 raise DDNSRequestError(_("Too frequent update requests have been sent."))
873
874 # If we got here, some other update error happened.
875 raise DDNSUpdateError
876
877
878 class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
879 handle = "domopoli.de"
880 name = "domopoli.de"
881 website = "http://domopoli.de/"
882 protocols = ("ipv4",)
883
884 # https://www.domopoli.de/?page=howto#DynDns_start
885
886 url = "http://dyndns.domopoli.de/nic/update"
887
888
889 class DDNSProviderDynsNet(DDNSProvider):
890 handle = "dyns.net"
891 name = "DyNS"
892 website = "http://www.dyns.net/"
893 protocols = ("ipv4",)
894 can_remove_records = False
895
896 # There is very detailed informatio about how to send the update request and
897 # the possible response codes. (Currently we are using the v1.1 proto)
898 # http://www.dyns.net/documentation/technical/protocol/
899
900 url = "http://www.dyns.net/postscript011.php"
901
902 def update_protocol(self, proto):
903 data = {
904 "ip" : self.get_address(proto),
905 "host" : self.hostname,
906 "username" : self.username,
907 "password" : self.password,
908 }
909
910 # Send update to the server.
911 response = self.send_request(self.url, data=data)
912
913 # Get the full response message.
914 output = response.read()
915
916 # Handle success messages.
917 if output.startswith("200"):
918 return
919
920 # Handle error codes.
921 if output.startswith("400"):
922 raise DDNSRequestError(_("Malformed request has been sent."))
923 elif output.startswith("401"):
924 raise DDNSAuthenticationError
925 elif output.startswith("402"):
926 raise DDNSRequestError(_("Too frequent update requests have been sent."))
927 elif output.startswith("403"):
928 raise DDNSInternalServerError
929
930 # If we got here, some other update error happened.
931 raise DDNSUpdateError(_("Server response: %s") % output)
932
933
934 class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
935 handle = "enom.com"
936 name = "eNom Inc."
937 website = "http://www.enom.com/"
938 protocols = ("ipv4",)
939
940 # There are very detailed information about how to send an update request and
941 # the respone codes.
942 # http://www.enom.com/APICommandCatalog/
943
944 url = "https://dynamic.name-services.com/interface.asp"
945 can_remove_records = False
946
947 def update_protocol(self, proto):
948 data = {
949 "command" : "setdnshost",
950 "responsetype" : "xml",
951 "address" : self.get_address(proto),
952 "domainpassword" : self.password,
953 "zone" : self.hostname
954 }
955
956 # Send update to the server.
957 response = self.send_request(self.url, data=data)
958
959 # Get the full response message.
960 output = response.read()
961
962 # Handle success messages.
963 if self.get_xml_tag_value(output, "ErrCount") == "0":
964 return
965
966 # Handle error codes.
967 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
968
969 if errorcode == "304155":
970 raise DDNSAuthenticationError
971 elif errorcode == "304153":
972 raise DDNSRequestError(_("Domain not found."))
973
974 # If we got here, some other update error happened.
975 raise DDNSUpdateError
976
977
978 class DDNSProviderEntryDNS(DDNSProvider):
979 handle = "entrydns.net"
980 name = "EntryDNS"
981 website = "http://entrydns.net/"
982 protocols = ("ipv4",)
983
984 # Some very tiny details about their so called "Simple API" can be found
985 # here: https://entrydns.net/help
986 url = "https://entrydns.net/records/modify"
987 can_remove_records = False
988
989 def update_protocol(self, proto):
990 data = {
991 "ip" : self.get_address(proto),
992 }
993
994 # Add auth token to the update url.
995 url = "%s/%s" % (self.url, self.token)
996
997 # Send update to the server.
998 try:
999 response = self.send_request(url, data=data)
1000
1001 # Handle error codes
1002 except urllib2.HTTPError, e:
1003 if e.code == 404:
1004 raise DDNSAuthenticationError
1005
1006 elif e.code == 422:
1007 raise DDNSRequestError(_("An invalid IP address was submitted"))
1008
1009 raise
1010
1011 # Handle success messages.
1012 if response.code == 200:
1013 return
1014
1015 # If we got here, some other update error happened.
1016 raise DDNSUpdateError
1017
1018
1019 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
1020 handle = "freedns.afraid.org"
1021 name = "freedns.afraid.org"
1022 website = "http://freedns.afraid.org/"
1023
1024 # No information about the request or response could be found on the vendor
1025 # page. All used values have been collected by testing.
1026 url = "https://freedns.afraid.org/dynamic/update.php"
1027 can_remove_records = False
1028
1029 def update_protocol(self, proto):
1030 data = {
1031 "address" : self.get_address(proto),
1032 }
1033
1034 # Add auth token to the update url.
1035 url = "%s?%s" % (self.url, self.token)
1036
1037 # Send update to the server.
1038 response = self.send_request(url, data=data)
1039
1040 # Get the full response message.
1041 output = response.read()
1042
1043 # Handle success messages.
1044 if output.startswith("Updated") or "has not changed" in output:
1045 return
1046
1047 # Handle error codes.
1048 if output == "ERROR: Unable to locate this record":
1049 raise DDNSAuthenticationError
1050 elif "is an invalid IP address" in output:
1051 raise DDNSRequestError(_("Invalid IP address has been sent."))
1052
1053 # If we got here, some other update error happened.
1054 raise DDNSUpdateError
1055
1056
1057 class DDNSProviderJoker(DDNSProtocolDynDNS2, DDNSProvider):
1058 handle = "joker.com"
1059 name = "Joker.com Dynamic DNS"
1060 website = "https://joker.com/"
1061 protocols = ("ipv4",)
1062
1063 # Information about the request can be found here:
1064 # https://joker.com/faq/content/11/427/en/what-is-dynamic-dns-dyndns.html
1065 # Using DynDNS V2 protocol over HTTPS here
1066
1067 url = "https://svc.joker.com/nic/update"
1068
1069
1070 class DDNSProviderGoogle(DDNSProtocolDynDNS2, DDNSProvider):
1071 handle = "domains.google.com"
1072 name = "Google Domains"
1073 website = "https://domains.google.com/"
1074 protocols = ("ipv4",)
1075
1076 # Information about the format of the HTTP request is to be found
1077 # here: https://support.google.com/domains/answer/6147083?hl=en
1078
1079 url = "https://domains.google.com/nic/update"
1080
1081
1082 class DDNSProviderLightningWireLabs(DDNSProvider):
1083 handle = "dns.lightningwirelabs.com"
1084 name = "Lightning Wire Labs DNS Service"
1085 website = "http://dns.lightningwirelabs.com/"
1086
1087 # Information about the format of the HTTPS request is to be found
1088 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
1089
1090 url = "https://dns.lightningwirelabs.com/update"
1091
1092 def update(self):
1093 data = {
1094 "hostname" : self.hostname,
1095 "address6" : self.get_address("ipv6", "-"),
1096 "address4" : self.get_address("ipv4", "-"),
1097 }
1098
1099 # Check if a token has been set.
1100 if self.token:
1101 data["token"] = self.token
1102
1103 # Check for username and password.
1104 elif self.username and self.password:
1105 data.update({
1106 "username" : self.username,
1107 "password" : self.password,
1108 })
1109
1110 # Raise an error if no auth details are given.
1111 else:
1112 raise DDNSConfigurationError
1113
1114 # Send update to the server.
1115 response = self.send_request(self.url, data=data)
1116
1117 # Handle success messages.
1118 if response.code == 200:
1119 return
1120
1121 # If we got here, some other update error happened.
1122 raise DDNSUpdateError
1123
1124
1125 class DDNSProviderLoopia(DDNSProtocolDynDNS2, DDNSProvider):
1126 handle = "loopia.se"
1127 name = "Loopia AB"
1128 website = "https://www.loopia.com"
1129 protocols = ("ipv4",)
1130
1131 # Information about the format of the HTTP request is to be found
1132 # here: https://support.loopia.com/wiki/About_the_DynDNS_support
1133
1134 url = "https://dns.loopia.se/XDynDNSServer/XDynDNS.php"
1135
1136
1137 class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
1138 handle = "myonlineportal.net"
1139 name = "myonlineportal.net"
1140 website = "https:/myonlineportal.net/"
1141
1142 # Information about the request and response can be obtained here:
1143 # https://myonlineportal.net/howto_dyndns
1144
1145 url = "https://myonlineportal.net/updateddns"
1146
1147 def prepare_request_data(self, proto):
1148 data = {
1149 "hostname" : self.hostname,
1150 "ip" : self.get_address(proto),
1151 }
1152
1153 return data
1154
1155
1156 class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
1157 handle = "namecheap.com"
1158 name = "Namecheap"
1159 website = "http://namecheap.com"
1160 protocols = ("ipv4",)
1161
1162 # Information about the format of the HTTP request is to be found
1163 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
1164 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
1165
1166 url = "https://dynamicdns.park-your-domain.com/update"
1167 can_remove_records = False
1168
1169 def update_protocol(self, proto):
1170 # Namecheap requires the hostname splitted into a host and domain part.
1171 host, domain = self.hostname.split(".", 1)
1172
1173 data = {
1174 "ip" : self.get_address(proto),
1175 "password" : self.password,
1176 "host" : host,
1177 "domain" : domain
1178 }
1179
1180 # Send update to the server.
1181 response = self.send_request(self.url, data=data)
1182
1183 # Get the full response message.
1184 output = response.read()
1185
1186 # Handle success messages.
1187 if self.get_xml_tag_value(output, "IP") == address:
1188 return
1189
1190 # Handle error codes.
1191 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
1192
1193 if errorcode == "304156":
1194 raise DDNSAuthenticationError
1195 elif errorcode == "316153":
1196 raise DDNSRequestError(_("Domain not found."))
1197 elif errorcode == "316154":
1198 raise DDNSRequestError(_("Domain not active."))
1199 elif errorcode in ("380098", "380099"):
1200 raise DDNSInternalServerError
1201
1202 # If we got here, some other update error happened.
1203 raise DDNSUpdateError
1204
1205
1206 class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
1207 handle = "no-ip.com"
1208 name = "No-IP"
1209 website = "http://www.no-ip.com/"
1210 protocols = ("ipv4",)
1211
1212 # Information about the format of the HTTP request is to be found
1213 # here: http://www.no-ip.com/integrate/request and
1214 # here: http://www.no-ip.com/integrate/response
1215
1216 url = "http://dynupdate.no-ip.com/nic/update"
1217
1218 def prepare_request_data(self, proto):
1219 assert proto == "ipv4"
1220
1221 data = {
1222 "hostname" : self.hostname,
1223 "address" : self.get_address(proto),
1224 }
1225
1226 return data
1227
1228
1229 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
1230 handle = "nsupdate.info"
1231 name = "nsupdate.info"
1232 website = "http://nsupdate.info/"
1233 protocols = ("ipv6", "ipv4",)
1234
1235 # Information about the format of the HTTP request can be found
1236 # after login on the provider user interface and here:
1237 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1238
1239 url = "https://nsupdate.info/nic/update"
1240
1241 # TODO nsupdate.info can actually do this, but the functionality
1242 # has not been implemented here, yet.
1243 can_remove_records = False
1244
1245 # After a failed update, there will be no retries
1246 # https://bugzilla.ipfire.org/show_bug.cgi?id=10603
1247 holdoff_failure_days = None
1248
1249 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1250 # and for the password a so called secret.
1251 @property
1252 def username(self):
1253 return self.get("hostname")
1254
1255 @property
1256 def password(self):
1257 return self.token or self.get("secret")
1258
1259 def prepare_request_data(self, proto):
1260 data = {
1261 "myip" : self.get_address(proto),
1262 }
1263
1264 return data
1265
1266
1267 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
1268 handle = "opendns.com"
1269 name = "OpenDNS"
1270 website = "http://www.opendns.com"
1271
1272 # Detailed information about the update request and possible
1273 # response codes can be obtained from here:
1274 # https://support.opendns.com/entries/23891440
1275
1276 url = "https://updates.opendns.com/nic/update"
1277
1278 def prepare_request_data(self, proto):
1279 data = {
1280 "hostname" : self.hostname,
1281 "myip" : self.get_address(proto),
1282 }
1283
1284 return data
1285
1286
1287 class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1288 handle = "ovh.com"
1289 name = "OVH"
1290 website = "http://www.ovh.com/"
1291 protocols = ("ipv4",)
1292
1293 # OVH only provides very limited information about how to
1294 # update a DynDNS host. They only provide the update url
1295 # on the their german subpage.
1296 #
1297 # http://hilfe.ovh.de/DomainDynHost
1298
1299 url = "https://www.ovh.com/nic/update"
1300
1301 def prepare_request_data(self, proto):
1302 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1303 data.update({
1304 "system" : "dyndns",
1305 })
1306
1307 return data
1308
1309
1310 class DDNSProviderRegfish(DDNSProvider):
1311 handle = "regfish.com"
1312 name = "Regfish GmbH"
1313 website = "http://www.regfish.com/"
1314
1315 # A full documentation to the providers api can be found here
1316 # but is only available in german.
1317 # https://www.regfish.de/domains/dyndns/dokumentation
1318
1319 url = "https://dyndns.regfish.de/"
1320 can_remove_records = False
1321
1322 def update(self):
1323 data = {
1324 "fqdn" : self.hostname,
1325 }
1326
1327 # Check if we update an IPv6 address.
1328 address6 = self.get_address("ipv6")
1329 if address6:
1330 data["ipv6"] = address6
1331
1332 # Check if we update an IPv4 address.
1333 address4 = self.get_address("ipv4")
1334 if address4:
1335 data["ipv4"] = address4
1336
1337 # Raise an error if none address is given.
1338 if not data.has_key("ipv6") and not data.has_key("ipv4"):
1339 raise DDNSConfigurationError
1340
1341 # Check if a token has been set.
1342 if self.token:
1343 data["token"] = self.token
1344
1345 # Raise an error if no token and no useranem and password
1346 # are given.
1347 elif not self.username and not self.password:
1348 raise DDNSConfigurationError(_("No Auth details specified."))
1349
1350 # HTTP Basic Auth is only allowed if no token is used.
1351 if self.token:
1352 # Send update to the server.
1353 response = self.send_request(self.url, data=data)
1354 else:
1355 # Send update to the server.
1356 response = self.send_request(self.url, username=self.username, password=self.password,
1357 data=data)
1358
1359 # Get the full response message.
1360 output = response.read()
1361
1362 # Handle success messages.
1363 if "100" in output or "101" in output:
1364 return
1365
1366 # Handle error codes.
1367 if "401" or "402" in output:
1368 raise DDNSAuthenticationError
1369 elif "408" in output:
1370 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1371 elif "409" in output:
1372 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1373 elif "412" in output:
1374 raise DDNSRequestError(_("No valid FQDN was given."))
1375 elif "414" in output:
1376 raise DDNSInternalServerError
1377
1378 # If we got here, some other update error happened.
1379 raise DDNSUpdateError
1380
1381
1382 class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
1383 handle = "selfhost.de"
1384 name = "Selfhost.de"
1385 website = "http://www.selfhost.de/"
1386 protocols = ("ipv4",)
1387
1388 url = "https://carol.selfhost.de/nic/update"
1389
1390 def prepare_request_data(self, proto):
1391 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1392 data.update({
1393 "hostname" : "1",
1394 })
1395
1396 return data
1397
1398
1399 class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1400 handle = "spdns.org"
1401 name = "SPDNS"
1402 website = "http://spdns.org/"
1403
1404 # Detailed information about request and response codes are provided
1405 # by the vendor. They are using almost the same mechanism and status
1406 # codes as dyndns.org so we can inherit all those stuff.
1407 #
1408 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1409 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1410
1411 url = "https://update.spdns.de/nic/update"
1412
1413 @property
1414 def username(self):
1415 return self.get("username") or self.hostname
1416
1417 @property
1418 def password(self):
1419 return self.get("password") or self.token
1420
1421
1422 class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1423 handle = "strato.com"
1424 name = "Strato AG"
1425 website = "http:/www.strato.com/"
1426 protocols = ("ipv4",)
1427
1428 # Information about the request and response can be obtained here:
1429 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1430
1431 url = "https://dyndns.strato.com/nic/update"
1432
1433 def prepare_request_data(self, proto):
1434 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1435 data.update({
1436 "mx" : "NOCHG",
1437 "backupmx" : "NOCHG"
1438 })
1439
1440 return data
1441
1442
1443 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1444 handle = "twodns.de"
1445 name = "TwoDNS"
1446 website = "http://www.twodns.de"
1447 protocols = ("ipv4",)
1448
1449 # Detailed information about the request can be found here
1450 # http://twodns.de/en/faqs
1451 # http://twodns.de/en/api
1452
1453 url = "https://update.twodns.de/update"
1454
1455 def prepare_request_data(self, proto):
1456 assert proto == "ipv4"
1457
1458 data = {
1459 "ip" : self.get_address(proto),
1460 "hostname" : self.hostname
1461 }
1462
1463 return data
1464
1465
1466 class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1467 handle = "udmedia.de"
1468 name = "Udmedia GmbH"
1469 website = "http://www.udmedia.de"
1470 protocols = ("ipv4",)
1471
1472 # Information about the request can be found here
1473 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1474
1475 url = "https://www.udmedia.de/nic/update"
1476
1477
1478 class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
1479 handle = "variomedia.de"
1480 name = "Variomedia"
1481 website = "http://www.variomedia.de/"
1482 protocols = ("ipv6", "ipv4",)
1483
1484 # Detailed information about the request can be found here
1485 # https://dyndns.variomedia.de/
1486
1487 url = "https://dyndns.variomedia.de/nic/update"
1488
1489 def prepare_request_data(self, proto):
1490 data = {
1491 "hostname" : self.hostname,
1492 "myip" : self.get_address(proto),
1493 }
1494
1495 return data
1496
1497
1498 class DDNSProviderXLhost(DDNSProtocolDynDNS2, DDNSProvider):
1499 handle = "xlhost.de"
1500 name = "XLhost"
1501 website = "http://xlhost.de/"
1502 protocols = ("ipv4",)
1503
1504 # Information about the format of the HTTP request is to be found
1505 # here: https://xlhost.de/faq/index_html?topicId=CQA2ELIPO4SQ
1506
1507 url = "https://nsupdate.xlhost.de/"
1508
1509
1510 class DDNSProviderZoneedit(DDNSProvider):
1511 handle = "zoneedit.com"
1512 name = "Zoneedit"
1513 website = "http://www.zoneedit.com"
1514 protocols = ("ipv4",)
1515
1516 # Detailed information about the request and the response codes can be
1517 # obtained here:
1518 # http://www.zoneedit.com/doc/api/other.html
1519 # http://www.zoneedit.com/faq.html
1520
1521 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1522
1523 def update_protocol(self, proto):
1524 data = {
1525 "dnsto" : self.get_address(proto),
1526 "host" : self.hostname
1527 }
1528
1529 # Send update to the server.
1530 response = self.send_request(self.url, username=self.username, password=self.password,
1531 data=data)
1532
1533 # Get the full response message.
1534 output = response.read()
1535
1536 # Handle success messages.
1537 if output.startswith("<SUCCESS"):
1538 return
1539
1540 # Handle error codes.
1541 if output.startswith("invalid login"):
1542 raise DDNSAuthenticationError
1543 elif output.startswith("<ERROR CODE=\"704\""):
1544 raise DDNSRequestError(_("No valid FQDN was given."))
1545 elif output.startswith("<ERROR CODE=\"702\""):
1546 raise DDNSInternalServerError
1547
1548 # If we got here, some other update error happened.
1549 raise DDNSUpdateError
1550
1551
1552 class DDNSProviderZZZZ(DDNSProvider):
1553 handle = "zzzz.io"
1554 name = "zzzz"
1555 website = "https://zzzz.io"
1556 protocols = ("ipv6", "ipv4",)
1557
1558 # Detailed information about the update request can be found here:
1559 # https://zzzz.io/faq/
1560
1561 # Details about the possible response codes have been provided in the bugtracker:
1562 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1563
1564 url = "https://zzzz.io/api/v1/update"
1565 can_remove_records = False
1566
1567 def update_protocol(self, proto):
1568 data = {
1569 "ip" : self.get_address(proto),
1570 "token" : self.token,
1571 }
1572
1573 if proto == "ipv6":
1574 data["type"] = "aaaa"
1575
1576 # zzzz uses the host from the full hostname as part
1577 # of the update url.
1578 host, domain = self.hostname.split(".", 1)
1579
1580 # Add host value to the update url.
1581 url = "%s/%s" % (self.url, host)
1582
1583 # Send update to the server.
1584 try:
1585 response = self.send_request(url, data=data)
1586
1587 # Handle error codes.
1588 except DDNSNotFound:
1589 raise DDNSRequestError(_("Invalid hostname specified"))
1590
1591 # Handle success messages.
1592 if response.code == 200:
1593 return
1594
1595 # If we got here, some other update error happened.
1596 raise DDNSUpdateError