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