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