]> git.ipfire.org Git - ddns.git/blob - src/ddns/providers.py
Merge branch 'zoneedit-10645'
[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 # 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
339 class 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
399 class 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
428 class 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
458 class 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
542 class DDNSProviderChangeIP(DDNSProvider):
543 handle = "changeip.com"
544 name = "ChangeIP.com"
545 website = "https://changeip.com"
546 protocols = ("ipv4",)
547
548 # Detailed information about the update api can be found here.
549 # http://www.changeip.com/accounts/knowledgebase.php?action=displayarticle&id=34
550
551 url = "https://nic.changeip.com/nic/update"
552 can_remove_records = False
553
554 def update_protocol(self, proto):
555 data = {
556 "hostname" : self.hostname,
557 "myip" : self.get_address(proto),
558 }
559
560 # Send update to the server.
561 try:
562 response = self.send_request(self.url, username=self.username, password=self.password,
563 data=data)
564
565 # Handle error codes.
566 except urllib2.HTTPError, e:
567 if e.code == 422:
568 raise DDNSRequestError(_("Domain not found."))
569
570 raise
571
572 # Handle success message.
573 if response.code == 200:
574 return
575
576 # If we got here, some other update error happened.
577 raise DDNSUpdateError(_("Server response: %s") % output)
578
579
580 class DDNSProviderDHS(DDNSProvider):
581 handle = "dhs.org"
582 name = "DHS International"
583 website = "http://dhs.org/"
584 protocols = ("ipv4",)
585
586 # No information about the used update api provided on webpage,
587 # grabed from source code of ez-ipudate.
588
589 url = "http://members.dhs.org/nic/hosts"
590 can_remove_records = False
591
592 def update_protocol(self, proto):
593 data = {
594 "domain" : self.hostname,
595 "ip" : self.get_address(proto),
596 "hostcmd" : "edit",
597 "hostcmdstage" : "2",
598 "type" : "4",
599 }
600
601 # Send update to the server.
602 response = self.send_request(self.url, username=self.username, password=self.password,
603 data=data)
604
605 # Handle success messages.
606 if response.code == 200:
607 return
608
609 # If we got here, some other update error happened.
610 raise DDNSUpdateError
611
612
613 class DDNSProviderDNSpark(DDNSProvider):
614 handle = "dnspark.com"
615 name = "DNS Park"
616 website = "http://dnspark.com/"
617 protocols = ("ipv4",)
618
619 # Informations to the used api can be found here:
620 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
621
622 url = "https://control.dnspark.com/api/dynamic/update.php"
623 can_remove_records = False
624
625 def update_protocol(self, proto):
626 data = {
627 "domain" : self.hostname,
628 "ip" : self.get_address(proto),
629 }
630
631 # Send update to the server.
632 response = self.send_request(self.url, username=self.username, password=self.password,
633 data=data)
634
635 # Get the full response message.
636 output = response.read()
637
638 # Handle success messages.
639 if output.startswith("ok") or output.startswith("nochange"):
640 return
641
642 # Handle error codes.
643 if output == "unauth":
644 raise DDNSAuthenticationError
645 elif output == "abuse":
646 raise DDNSAbuseError
647 elif output == "blocked":
648 raise DDNSBlockedError
649 elif output == "nofqdn":
650 raise DDNSRequestError(_("No valid FQDN was given."))
651 elif output == "nohost":
652 raise DDNSRequestError(_("Invalid hostname specified."))
653 elif output == "notdyn":
654 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
655 elif output == "invalid":
656 raise DDNSRequestError(_("Invalid IP address has been sent."))
657
658 # If we got here, some other update error happened.
659 raise DDNSUpdateError
660
661
662 class DDNSProviderDtDNS(DDNSProvider):
663 handle = "dtdns.com"
664 name = "DtDNS"
665 website = "http://dtdns.com/"
666 protocols = ("ipv4",)
667
668 # Information about the format of the HTTPS request is to be found
669 # http://www.dtdns.com/dtsite/updatespec
670
671 url = "https://www.dtdns.com/api/autodns.cfm"
672 can_remove_records = False
673
674 def update_protocol(self, proto):
675 data = {
676 "ip" : self.get_address(proto),
677 "id" : self.hostname,
678 "pw" : self.password
679 }
680
681 # Send update to the server.
682 response = self.send_request(self.url, data=data)
683
684 # Get the full response message.
685 output = response.read()
686
687 # Remove all leading and trailing whitespace.
688 output = output.strip()
689
690 # Handle success messages.
691 if "now points to" in output:
692 return
693
694 # Handle error codes.
695 if output == "No hostname to update was supplied.":
696 raise DDNSRequestError(_("No hostname specified."))
697
698 elif output == "The hostname you supplied is not valid.":
699 raise DDNSRequestError(_("Invalid hostname specified."))
700
701 elif output == "The password you supplied is not valid.":
702 raise DDNSAuthenticationError
703
704 elif output == "Administration has disabled this account.":
705 raise DDNSRequestError(_("Account has been disabled."))
706
707 elif output == "Illegal character in IP.":
708 raise DDNSRequestError(_("Invalid IP address has been sent."))
709
710 elif output == "Too many failed requests.":
711 raise DDNSRequestError(_("Too many failed requests."))
712
713 # If we got here, some other update error happened.
714 raise DDNSUpdateError
715
716
717 class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
718 handle = "dyndns.org"
719 name = "Dyn"
720 website = "http://dyn.com/dns/"
721 protocols = ("ipv4",)
722
723 # Information about the format of the request is to be found
724 # http://http://dyn.com/support/developers/api/perform-update/
725 # http://dyn.com/support/developers/api/return-codes/
726
727 url = "https://members.dyndns.org/nic/update"
728
729
730 class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
731 handle = "dynu.com"
732 name = "Dynu"
733 website = "http://dynu.com/"
734 protocols = ("ipv6", "ipv4",)
735
736 # Detailed information about the request and response codes
737 # are available on the providers webpage.
738 # http://dynu.com/Default.aspx?page=dnsapi
739
740 url = "https://api.dynu.com/nic/update"
741
742 # DynU sends the IPv6 and IPv4 address in one request
743
744 def update(self):
745 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
746
747 # This one supports IPv6
748 myipv6 = self.get_address("ipv6")
749
750 # Add update information if we have an IPv6 address.
751 if myipv6:
752 data["myipv6"] = myipv6
753
754 self.send_request(data)
755
756
757 class DDNSProviderEasyDNS(DDNSProvider):
758 handle = "easydns.com"
759 name = "EasyDNS"
760 website = "http://www.easydns.com/"
761 protocols = ("ipv4",)
762
763 # Detailed information about the request and response codes
764 # (API 1.3) are available on the providers webpage.
765 # https://fusion.easydns.com/index.php?/Knowledgebase/Article/View/102/7/dynamic-dns
766
767 url = "http://api.cp.easydns.com/dyn/tomato.php"
768
769 def update_protocol(self, proto):
770 data = {
771 "myip" : self.get_address(proto, "-"),
772 "hostname" : self.hostname,
773 }
774
775 # Send update to the server.
776 response = self.send_request(self.url, data=data,
777 username=self.username, password=self.password)
778
779 # Get the full response message.
780 output = response.read()
781
782 # Remove all leading and trailing whitespace.
783 output = output.strip()
784
785 # Handle success messages.
786 if output.startswith("NOERROR"):
787 return
788
789 # Handle error codes.
790 if output.startswith("NOACCESS"):
791 raise DDNSAuthenticationError
792
793 elif output.startswith("NOSERVICE"):
794 raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain."))
795
796 elif output.startswith("ILLEGAL INPUT"):
797 raise DDNSRequestError(_("Invalid data has been sent."))
798
799 elif output.startswith("TOOSOON"):
800 raise DDNSRequestError(_("Too frequent update requests have been sent."))
801
802 # If we got here, some other update error happened.
803 raise DDNSUpdateError
804
805
806 class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
807 handle = "domopoli.de"
808 name = "domopoli.de"
809 website = "http://domopoli.de/"
810 protocols = ("ipv4",)
811
812 # https://www.domopoli.de/?page=howto#DynDns_start
813
814 url = "http://dyndns.domopoli.de/nic/update"
815
816
817 class DDNSProviderDynsNet(DDNSProvider):
818 handle = "dyns.net"
819 name = "DyNS"
820 website = "http://www.dyns.net/"
821 protocols = ("ipv4",)
822 can_remove_records = False
823
824 # There is very detailed informatio about how to send the update request and
825 # the possible response codes. (Currently we are using the v1.1 proto)
826 # http://www.dyns.net/documentation/technical/protocol/
827
828 url = "http://www.dyns.net/postscript011.php"
829
830 def update_protocol(self, proto):
831 data = {
832 "ip" : self.get_address(proto),
833 "host" : self.hostname,
834 "username" : self.username,
835 "password" : self.password,
836 }
837
838 # Send update to the server.
839 response = self.send_request(self.url, data=data)
840
841 # Get the full response message.
842 output = response.read()
843
844 # Handle success messages.
845 if output.startswith("200"):
846 return
847
848 # Handle error codes.
849 if output.startswith("400"):
850 raise DDNSRequestError(_("Malformed request has been sent."))
851 elif output.startswith("401"):
852 raise DDNSAuthenticationError
853 elif output.startswith("402"):
854 raise DDNSRequestError(_("Too frequent update requests have been sent."))
855 elif output.startswith("403"):
856 raise DDNSInternalServerError
857
858 # If we got here, some other update error happened.
859 raise DDNSUpdateError(_("Server response: %s") % output)
860
861
862 class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
863 handle = "enom.com"
864 name = "eNom Inc."
865 website = "http://www.enom.com/"
866 protocols = ("ipv4",)
867
868 # There are very detailed information about how to send an update request and
869 # the respone codes.
870 # http://www.enom.com/APICommandCatalog/
871
872 url = "https://dynamic.name-services.com/interface.asp"
873 can_remove_records = False
874
875 def update_protocol(self, proto):
876 data = {
877 "command" : "setdnshost",
878 "responsetype" : "xml",
879 "address" : self.get_address(proto),
880 "domainpassword" : self.password,
881 "zone" : self.hostname
882 }
883
884 # Send update to the server.
885 response = self.send_request(self.url, data=data)
886
887 # Get the full response message.
888 output = response.read()
889
890 # Handle success messages.
891 if self.get_xml_tag_value(output, "ErrCount") == "0":
892 return
893
894 # Handle error codes.
895 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
896
897 if errorcode == "304155":
898 raise DDNSAuthenticationError
899 elif errorcode == "304153":
900 raise DDNSRequestError(_("Domain not found."))
901
902 # If we got here, some other update error happened.
903 raise DDNSUpdateError
904
905
906 class DDNSProviderEntryDNS(DDNSProvider):
907 handle = "entrydns.net"
908 name = "EntryDNS"
909 website = "http://entrydns.net/"
910 protocols = ("ipv4",)
911
912 # Some very tiny details about their so called "Simple API" can be found
913 # here: https://entrydns.net/help
914 url = "https://entrydns.net/records/modify"
915 can_remove_records = False
916
917 def update_protocol(self, proto):
918 data = {
919 "ip" : self.get_address(proto),
920 }
921
922 # Add auth token to the update url.
923 url = "%s/%s" % (self.url, self.token)
924
925 # Send update to the server.
926 try:
927 response = self.send_request(url, data=data)
928
929 # Handle error codes
930 except urllib2.HTTPError, e:
931 if e.code == 404:
932 raise DDNSAuthenticationError
933
934 elif e.code == 422:
935 raise DDNSRequestError(_("An invalid IP address was submitted"))
936
937 raise
938
939 # Handle success messages.
940 if response.code == 200:
941 return
942
943 # If we got here, some other update error happened.
944 raise DDNSUpdateError
945
946
947 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
948 handle = "freedns.afraid.org"
949 name = "freedns.afraid.org"
950 website = "http://freedns.afraid.org/"
951
952 # No information about the request or response could be found on the vendor
953 # page. All used values have been collected by testing.
954 url = "https://freedns.afraid.org/dynamic/update.php"
955 can_remove_records = False
956
957 def update_protocol(self, proto):
958 data = {
959 "address" : self.get_address(proto),
960 }
961
962 # Add auth token to the update url.
963 url = "%s?%s" % (self.url, self.token)
964
965 # Send update to the server.
966 response = self.send_request(url, data=data)
967
968 # Get the full response message.
969 output = response.read()
970
971 # Handle success messages.
972 if output.startswith("Updated") or "has not changed" in output:
973 return
974
975 # Handle error codes.
976 if output == "ERROR: Unable to locate this record":
977 raise DDNSAuthenticationError
978 elif "is an invalid IP address" in output:
979 raise DDNSRequestError(_("Invalid IP address has been sent."))
980
981 # If we got here, some other update error happened.
982 raise DDNSUpdateError
983
984
985 class DDNSProviderLightningWireLabs(DDNSProvider):
986 handle = "dns.lightningwirelabs.com"
987 name = "Lightning Wire Labs DNS Service"
988 website = "http://dns.lightningwirelabs.com/"
989
990 # Information about the format of the HTTPS request is to be found
991 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
992
993 url = "https://dns.lightningwirelabs.com/update"
994
995 def update(self):
996 data = {
997 "hostname" : self.hostname,
998 "address6" : self.get_address("ipv6", "-"),
999 "address4" : self.get_address("ipv4", "-"),
1000 }
1001
1002 # Check if a token has been set.
1003 if self.token:
1004 data["token"] = self.token
1005
1006 # Check for username and password.
1007 elif self.username and self.password:
1008 data.update({
1009 "username" : self.username,
1010 "password" : self.password,
1011 })
1012
1013 # Raise an error if no auth details are given.
1014 else:
1015 raise DDNSConfigurationError
1016
1017 # Send update to the server.
1018 response = self.send_request(self.url, data=data)
1019
1020 # Handle success messages.
1021 if response.code == 200:
1022 return
1023
1024 # If we got here, some other update error happened.
1025 raise DDNSUpdateError
1026
1027
1028 class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
1029 handle = "myonlineportal.net"
1030 name = "myonlineportal.net"
1031 website = "https:/myonlineportal.net/"
1032
1033 # Information about the request and response can be obtained here:
1034 # https://myonlineportal.net/howto_dyndns
1035
1036 url = "https://myonlineportal.net/updateddns"
1037
1038 def prepare_request_data(self, proto):
1039 data = {
1040 "hostname" : self.hostname,
1041 "ip" : self.get_address(proto),
1042 }
1043
1044 return data
1045
1046
1047 class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
1048 handle = "namecheap.com"
1049 name = "Namecheap"
1050 website = "http://namecheap.com"
1051 protocols = ("ipv4",)
1052
1053 # Information about the format of the HTTP request is to be found
1054 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
1055 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
1056
1057 url = "https://dynamicdns.park-your-domain.com/update"
1058 can_remove_records = False
1059
1060 def update_protocol(self, proto):
1061 # Namecheap requires the hostname splitted into a host and domain part.
1062 host, domain = self.hostname.split(".", 1)
1063
1064 data = {
1065 "ip" : self.get_address(proto),
1066 "password" : self.password,
1067 "host" : host,
1068 "domain" : domain
1069 }
1070
1071 # Send update to the server.
1072 response = self.send_request(self.url, data=data)
1073
1074 # Get the full response message.
1075 output = response.read()
1076
1077 # Handle success messages.
1078 if self.get_xml_tag_value(output, "IP") == address:
1079 return
1080
1081 # Handle error codes.
1082 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
1083
1084 if errorcode == "304156":
1085 raise DDNSAuthenticationError
1086 elif errorcode == "316153":
1087 raise DDNSRequestError(_("Domain not found."))
1088 elif errorcode == "316154":
1089 raise DDNSRequestError(_("Domain not active."))
1090 elif errorcode in ("380098", "380099"):
1091 raise DDNSInternalServerError
1092
1093 # If we got here, some other update error happened.
1094 raise DDNSUpdateError
1095
1096
1097 class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
1098 handle = "no-ip.com"
1099 name = "No-IP"
1100 website = "http://www.no-ip.com/"
1101 protocols = ("ipv4",)
1102
1103 # Information about the format of the HTTP request is to be found
1104 # here: http://www.no-ip.com/integrate/request and
1105 # here: http://www.no-ip.com/integrate/response
1106
1107 url = "http://dynupdate.no-ip.com/nic/update"
1108
1109 def prepare_request_data(self, proto):
1110 assert proto == "ipv4"
1111
1112 data = {
1113 "hostname" : self.hostname,
1114 "address" : self.get_address(proto),
1115 }
1116
1117 return data
1118
1119
1120 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
1121 handle = "nsupdate.info"
1122 name = "nsupdate.info"
1123 website = "http://nsupdate.info/"
1124 protocols = ("ipv6", "ipv4",)
1125
1126 # Information about the format of the HTTP request can be found
1127 # after login on the provider user interface and here:
1128 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1129
1130 url = "https://nsupdate.info/nic/update"
1131
1132 # TODO nsupdate.info can actually do this, but the functionality
1133 # has not been implemented here, yet.
1134 can_remove_records = False
1135
1136 # After a failed update, there will be no retries
1137 # https://bugzilla.ipfire.org/show_bug.cgi?id=10603
1138 holdoff_failure_days = None
1139
1140 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1141 # and for the password a so called secret.
1142 @property
1143 def username(self):
1144 return self.get("hostname")
1145
1146 @property
1147 def password(self):
1148 return self.token or self.get("secret")
1149
1150 def prepare_request_data(self, proto):
1151 data = {
1152 "myip" : self.get_address(proto),
1153 }
1154
1155 return data
1156
1157
1158 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
1159 handle = "opendns.com"
1160 name = "OpenDNS"
1161 website = "http://www.opendns.com"
1162
1163 # Detailed information about the update request and possible
1164 # response codes can be obtained from here:
1165 # https://support.opendns.com/entries/23891440
1166
1167 url = "https://updates.opendns.com/nic/update"
1168
1169 def prepare_request_data(self, proto):
1170 data = {
1171 "hostname" : self.hostname,
1172 "myip" : self.get_address(proto),
1173 }
1174
1175 return data
1176
1177
1178 class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1179 handle = "ovh.com"
1180 name = "OVH"
1181 website = "http://www.ovh.com/"
1182 protocols = ("ipv4",)
1183
1184 # OVH only provides very limited information about how to
1185 # update a DynDNS host. They only provide the update url
1186 # on the their german subpage.
1187 #
1188 # http://hilfe.ovh.de/DomainDynHost
1189
1190 url = "https://www.ovh.com/nic/update"
1191
1192 def prepare_request_data(self, proto):
1193 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1194 data.update({
1195 "system" : "dyndns",
1196 })
1197
1198 return data
1199
1200
1201 class DDNSProviderRegfish(DDNSProvider):
1202 handle = "regfish.com"
1203 name = "Regfish GmbH"
1204 website = "http://www.regfish.com/"
1205
1206 # A full documentation to the providers api can be found here
1207 # but is only available in german.
1208 # https://www.regfish.de/domains/dyndns/dokumentation
1209
1210 url = "https://dyndns.regfish.de/"
1211 can_remove_records = False
1212
1213 def update(self):
1214 data = {
1215 "fqdn" : self.hostname,
1216 }
1217
1218 # Check if we update an IPv6 address.
1219 address6 = self.get_address("ipv6")
1220 if address6:
1221 data["ipv6"] = address6
1222
1223 # Check if we update an IPv4 address.
1224 address4 = self.get_address("ipv4")
1225 if address4:
1226 data["ipv4"] = address4
1227
1228 # Raise an error if none address is given.
1229 if not data.has_key("ipv6") and not data.has_key("ipv4"):
1230 raise DDNSConfigurationError
1231
1232 # Check if a token has been set.
1233 if self.token:
1234 data["token"] = self.token
1235
1236 # Raise an error if no token and no useranem and password
1237 # are given.
1238 elif not self.username and not self.password:
1239 raise DDNSConfigurationError(_("No Auth details specified."))
1240
1241 # HTTP Basic Auth is only allowed if no token is used.
1242 if self.token:
1243 # Send update to the server.
1244 response = self.send_request(self.url, data=data)
1245 else:
1246 # Send update to the server.
1247 response = self.send_request(self.url, username=self.username, password=self.password,
1248 data=data)
1249
1250 # Get the full response message.
1251 output = response.read()
1252
1253 # Handle success messages.
1254 if "100" in output or "101" in output:
1255 return
1256
1257 # Handle error codes.
1258 if "401" or "402" in output:
1259 raise DDNSAuthenticationError
1260 elif "408" in output:
1261 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1262 elif "409" in output:
1263 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1264 elif "412" in output:
1265 raise DDNSRequestError(_("No valid FQDN was given."))
1266 elif "414" in output:
1267 raise DDNSInternalServerError
1268
1269 # If we got here, some other update error happened.
1270 raise DDNSUpdateError
1271
1272
1273 class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
1274 handle = "selfhost.de"
1275 name = "Selfhost.de"
1276 website = "http://www.selfhost.de/"
1277 protocols = ("ipv4",)
1278
1279 url = "https://carol.selfhost.de/nic/update"
1280
1281 def prepare_request_data(self, proto):
1282 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1283 data.update({
1284 "hostname" : "1",
1285 })
1286
1287 return data
1288
1289
1290 class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1291 handle = "spdns.org"
1292 name = "SPDNS"
1293 website = "http://spdns.org/"
1294
1295 # Detailed information about request and response codes are provided
1296 # by the vendor. They are using almost the same mechanism and status
1297 # codes as dyndns.org so we can inherit all those stuff.
1298 #
1299 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1300 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1301
1302 url = "https://update.spdns.de/nic/update"
1303
1304 @property
1305 def username(self):
1306 return self.get("username") or self.hostname
1307
1308 @property
1309 def password(self):
1310 return self.get("password") or self.token
1311
1312
1313 class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1314 handle = "strato.com"
1315 name = "Strato AG"
1316 website = "http:/www.strato.com/"
1317 protocols = ("ipv4",)
1318
1319 # Information about the request and response can be obtained here:
1320 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1321
1322 url = "https://dyndns.strato.com/nic/update"
1323
1324
1325 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1326 handle = "twodns.de"
1327 name = "TwoDNS"
1328 website = "http://www.twodns.de"
1329 protocols = ("ipv4",)
1330
1331 # Detailed information about the request can be found here
1332 # http://twodns.de/en/faqs
1333 # http://twodns.de/en/api
1334
1335 url = "https://update.twodns.de/update"
1336
1337 def prepare_request_data(self, proto):
1338 assert proto == "ipv4"
1339
1340 data = {
1341 "ip" : self.get_address(proto),
1342 "hostname" : self.hostname
1343 }
1344
1345 return data
1346
1347
1348 class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1349 handle = "udmedia.de"
1350 name = "Udmedia GmbH"
1351 website = "http://www.udmedia.de"
1352 protocols = ("ipv4",)
1353
1354 # Information about the request can be found here
1355 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1356
1357 url = "https://www.udmedia.de/nic/update"
1358
1359
1360 class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
1361 handle = "variomedia.de"
1362 name = "Variomedia"
1363 website = "http://www.variomedia.de/"
1364 protocols = ("ipv6", "ipv4",)
1365
1366 # Detailed information about the request can be found here
1367 # https://dyndns.variomedia.de/
1368
1369 url = "https://dyndns.variomedia.de/nic/update"
1370
1371 def prepare_request_data(self, proto):
1372 data = {
1373 "hostname" : self.hostname,
1374 "myip" : self.get_address(proto),
1375 }
1376
1377 return data
1378
1379
1380 class DDNSProviderZoneedit(DDNSProvider):
1381 handle = "zoneedit.com"
1382 name = "Zoneedit"
1383 website = "http://www.zoneedit.com"
1384 protocols = ("ipv4",)
1385
1386 # Detailed information about the request and the response codes can be
1387 # obtained here:
1388 # http://www.zoneedit.com/doc/api/other.html
1389 # http://www.zoneedit.com/faq.html
1390
1391 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1392
1393 def update_protocol(self, proto):
1394 data = {
1395 "dnsto" : self.get_address(proto),
1396 "host" : self.hostname
1397 }
1398
1399 # Send update to the server.
1400 response = self.send_request(self.url, username=self.username, password=self.password,
1401 data=data)
1402
1403 # Get the full response message.
1404 output = response.read()
1405
1406 # Handle success messages.
1407 if output.startswith("<SUCCESS"):
1408 return
1409
1410 # Handle error codes.
1411 if output.startswith("invalid login"):
1412 raise DDNSAuthenticationError
1413 elif output.startswith("<ERROR CODE=\"704\""):
1414 raise DDNSRequestError(_("No valid FQDN was given."))
1415 elif output.startswith("<ERROR CODE=\"702\""):
1416 raise DDNSInternalServerError
1417
1418 # If we got here, some other update error happened.
1419 raise DDNSUpdateError
1420
1421
1422 class DDNSProviderZZZZ(DDNSProvider):
1423 handle = "zzzz.io"
1424 name = "zzzz"
1425 website = "https://zzzz.io"
1426 protocols = ("ipv6", "ipv4",)
1427
1428 # Detailed information about the update request can be found here:
1429 # https://zzzz.io/faq/
1430
1431 # Details about the possible response codes have been provided in the bugtracker:
1432 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1433
1434 url = "https://zzzz.io/api/v1/update"
1435 can_remove_records = False
1436
1437 def update_protocol(self, proto):
1438 data = {
1439 "ip" : self.get_address(proto),
1440 "token" : self.token,
1441 }
1442
1443 if proto == "ipv6":
1444 data["type"] = "aaaa"
1445
1446 # zzzz uses the host from the full hostname as part
1447 # of the update url.
1448 host, domain = self.hostname.split(".", 1)
1449
1450 # Add host value to the update url.
1451 url = "%s/%s" % (self.url, host)
1452
1453 # Send update to the server.
1454 try:
1455 response = self.send_request(url, data=data)
1456
1457 # Handle error codes.
1458 except DDNSNotFound:
1459 raise DDNSRequestError(_("Invalid hostname specified"))
1460
1461 # Handle success messages.
1462 if response.code == 200:
1463 return
1464
1465 # If we got here, some other update error happened.
1466 raise DDNSUpdateError