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