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