]> git.ipfire.org Git - oddments/ddns.git/blob - src/ddns/providers.py
b0066d5e699b2346c888b5ca1f91e7aa2e9bdfee
[oddments/ddns.git] / src / ddns / providers.py
1 #!/usr/bin/python3
2 ###############################################################################
3 # #
4 # ddns - A dynamic DNS client for IPFire #
5 # Copyright (C) 2012-2017 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 urllib.request
27 import urllib.error
28 import urllib.parse
29 import xml.dom.minidom
30
31 from .i18n import _
32
33 # Import all possible exception types.
34 from .errors import *
35
36 logger = logging.getLogger("ddns.providers")
37 logger.propagate = 1
38
39 _providers = {}
40
41 def get():
42 """
43 Returns a dict with all automatically registered providers.
44 """
45 return _providers.copy()
46
47 class DDNSProvider(object):
48 # A short string that uniquely identifies
49 # this provider.
50 handle = None
51
52 # The full name of the provider.
53 name = None
54
55 # A weburl to the homepage of the provider.
56 # (Where to register a new account?)
57 website = None
58
59 # A list of supported protocols.
60 protocols = ("ipv6", "ipv4")
61
62 DEFAULT_SETTINGS = {}
63
64 # holdoff time - Number of days no update is performed unless
65 # the IP address has changed.
66 holdoff_days = 30
67
68 # holdoff time for update failures - Number of days no update
69 # is tried after the last one has failed.
70 holdoff_failure_days = 0.5
71
72 # True if the provider is able to remove records, too.
73 # Required to remove AAAA records if IPv6 is absent again.
74 can_remove_records = True
75
76 # True if the provider supports authentication via a random
77 # generated token instead of username and password.
78 supports_token_auth = True
79
80 @staticmethod
81 def supported():
82 """
83 Should be overwritten to check if the system the code is running
84 on has all the required tools to support this provider.
85 """
86 return True
87
88 def __init__(self, core, **settings):
89 self.core = core
90
91 # Copy a set of default settings and
92 # update them by those from the configuration file.
93 self.settings = self.DEFAULT_SETTINGS.copy()
94 self.settings.update(settings)
95
96 def __init_subclass__(cls, **kwargs):
97 super().__init_subclass__(**kwargs)
98
99 if not all((cls.handle, cls.name, cls.website)):
100 raise DDNSError(_("Provider is not properly configured"))
101
102 assert cls.handle not in _providers, \
103 "Provider '%s' has already been registered" % cls.handle
104
105 # Register class
106 _providers[cls.handle] = cls
107
108 def __repr__(self):
109 return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
110
111 def __cmp__(self, other):
112 return (lambda a, b: (a > b)-(a < b))(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) is performed because of an IP address change") %
204 {"hostname": self.hostname, "provider": self.name})
205
206 return True
207
208 # If the holdoff time has expired, an update is required, too
209 if self.holdoff_time_expired():
210 logger.debug(_("An update for %(hostname)s (%(provider)s) is performed because the holdoff time has expired") %
211 {"hostname": self.hostname, "provider": self.name})
212
213 return True
214
215 # Otherwise, we don't need to perform an update
216 logger.debug(_("No update required for %(hostname)s (%(provider)s)") %
217 {"hostname": self.hostname, "provider": self.name})
218
219 return False
220
221 @property
222 def has_failure(self):
223 """
224 Returns True when the last update has failed and no retry
225 should be performed, yet.
226 """
227 last_status = self.db.last_update_status(self.hostname)
228
229 # Return False if the last update has not failed.
230 if not last_status == "failure":
231 return False
232
233 # If there is no holdoff time, we won't update ever again.
234 if self.holdoff_failure_days is None:
235 logger.warning(_("An update has not been performed because earlier updates failed for %s") % self.hostname)
236 logger.warning(_("There will be no retries"))
237
238 return True
239
240 # Determine when the holdoff time ends
241 last_update = self.db.last_update(self.hostname, status=last_status)
242 holdoff_end = last_update + datetime.timedelta(days=self.holdoff_failure_days)
243
244 now = datetime.datetime.utcnow()
245 if now < holdoff_end:
246 failure_message = self.db.last_update_failure_message(self.hostname)
247
248 logger.warning(_("An update has not been performed because earlier updates failed for %s") % self.hostname)
249
250 if failure_message:
251 logger.warning(_("Last failure message:"))
252
253 for line in failure_message.splitlines():
254 logger.warning(" %s" % line)
255
256 logger.warning(_("Further updates will be withheld until %s") % holdoff_end)
257
258 return True
259
260 return False
261
262 def ip_address_changed(self, protos):
263 """
264 Returns True if this host is already up to date
265 and does not need to change the IP address on the
266 name server.
267 """
268 for proto in protos:
269 addresses = self.core.system.resolve(self.hostname, proto)
270 current_address = self.get_address(proto)
271
272 # Handle if the system has not got any IP address from a protocol
273 # (i.e. had full dual-stack connectivity which it has not any more)
274 if current_address is None:
275 # If addresses still exists in the DNS system and if this provider
276 # is able to remove records, we will do that.
277 if addresses and self.can_remove_records:
278 return True
279
280 # Otherwise, we cannot go on...
281 continue
282
283 if not current_address in addresses:
284 return True
285
286 return False
287
288 def holdoff_time_expired(self):
289 """
290 Returns true if the holdoff time has expired
291 and the host requires an update
292 """
293 # If no holdoff days is defined, we cannot go on
294 if not self.holdoff_days:
295 return False
296
297 # Get the timestamp of the last successfull update
298 last_update = self.db.last_update(self.hostname, status="success")
299
300 # If no timestamp has been recorded, no update has been
301 # performed. An update should be performed now.
302 if not last_update:
303 return True
304
305 # Determine when the holdoff time ends
306 holdoff_end = last_update + datetime.timedelta(days=self.holdoff_days)
307
308 now = datetime.datetime.utcnow()
309
310 if now >= holdoff_end:
311 logger.debug("The holdoff time has expired for %s" % self.hostname)
312 return True
313 else:
314 logger.debug("Updates for %s are held off until %s" %
315 (self.hostname, holdoff_end))
316 return False
317
318 def send_request(self, *args, **kwargs):
319 """
320 Proxy connection to the send request
321 method.
322 """
323 return self.core.system.send_request(*args, **kwargs)
324
325 def get_address(self, proto, default=None):
326 """
327 Proxy method to get the current IP address.
328 """
329 return self.core.system.get_address(proto) or default
330
331 def have_address(self, proto):
332 """
333 Returns True if an IP address for the given protocol
334 is known and usable.
335 """
336 address = self.get_address(proto)
337
338 if address:
339 return True
340
341 return False
342
343
344 class DDNSProtocolDynDNS2(object):
345 """
346 This is an abstract class that implements the DynDNS updater
347 protocol version 2. As this is a popular way to update dynamic
348 DNS records, this class is supposed make the provider classes
349 shorter and simpler.
350 """
351
352 # Information about the format of the request is to be found
353 # http://dyn.com/support/developers/api/perform-update/
354 # http://dyn.com/support/developers/api/return-codes/
355
356 # The DynDNS protocol version 2 does not allow to remove records
357 can_remove_records = False
358
359 # The DynDNS protocol version 2 only supports authentication via
360 # username and password.
361 supports_token_auth = 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, username=self.username, password=self.password)
379
380 # Get the full response message.
381 output = response.read().decode()
382
383 # Handle success messages.
384 if output.startswith("good") or output.startswith("nochg"):
385 return
386
387 # Handle error codes.
388 if output == "badauth":
389 raise DDNSAuthenticationError
390 elif output == "abuse":
391 raise DDNSAbuseError
392 elif output == "notfqdn":
393 raise DDNSRequestError(_("No valid FQDN was given"))
394 elif output == "nohost":
395 raise DDNSRequestError(_("Specified host does not exist"))
396 elif output == "911":
397 raise DDNSInternalServerError
398 elif output == "dnserr":
399 raise DDNSInternalServerError(_("DNS error encountered"))
400 elif output == "badagent":
401 raise DDNSBlockedError
402 elif output == "badip":
403 raise DDNSBlockedError
404
405 # If we got here, some other update error happened.
406 raise DDNSUpdateError(_("Server response: %s") % output)
407
408
409 class DDNSResponseParserXML(object):
410 """
411 This class provides a parser for XML responses which
412 will be sent by various providers. This class uses the python
413 shipped XML minidom module to walk through the XML tree and return
414 a requested element.
415 """
416
417 def get_xml_tag_value(self, document, content):
418 # Send input to the parser.
419 xmldoc = xml.dom.minidom.parseString(document)
420
421 # Get XML elements by the given content.
422 element = xmldoc.getElementsByTagName(content)
423
424 # If no element has been found, we directly can return None.
425 if not element:
426 return None
427
428 # Only get the first child from an element, even there are more than one.
429 firstchild = element[0].firstChild
430
431 # Get the value of the child.
432 value = firstchild.nodeValue
433
434 # Return the value.
435 return value
436
437
438 class DDNSProviderAllInkl(DDNSProvider):
439 handle = "all-inkl.com"
440 name = "All-inkl.com"
441 website = "http://all-inkl.com/"
442 protocols = ("ipv4",)
443
444 # There are only information provided by the vendor how to
445 # perform an update on a FRITZ Box. Grab requried informations
446 # from the net.
447 # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
448
449 url = "http://dyndns.kasserver.com"
450 can_remove_records = False
451 supports_token_auth = False
452
453 def update(self):
454 # There is no additional data required so we directly can
455 # send our request.
456 response = self.send_request(self.url, username=self.username, password=self.password)
457
458 # Get the full response message.
459 output = response.read().decode()
460
461 # Handle success messages.
462 if output.startswith("good") or output.startswith("nochg"):
463 return
464
465 # If we got here, some other update error happened.
466 raise DDNSUpdateError
467
468
469 class DDNSProviderBindNsupdate(DDNSProvider):
470 handle = "nsupdate"
471 name = "BIND nsupdate utility"
472 website = "http://en.wikipedia.org/wiki/Nsupdate"
473
474 DEFAULT_TTL = 60
475
476 supports_token_auth = False
477
478 @staticmethod
479 def supported():
480 # Search if the nsupdate utility is available
481 paths = os.environ.get("PATH")
482
483 for path in paths.split(":"):
484 executable = os.path.join(path, "nsupdate")
485
486 if os.path.exists(executable):
487 return True
488
489 return False
490
491 def update(self):
492 scriptlet = self.__make_scriptlet()
493
494 # -v enables TCP hence we transfer keys and other data that may
495 # exceed the size of one packet.
496 # -t sets the timeout
497 command = ["nsupdate", "-v", "-t", "60"]
498
499 p = subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
500 stdout, stderr = p.communicate(scriptlet)
501
502 if p.returncode == 0:
503 return
504
505 raise DDNSError("nsupdate terminated with error code: %s\n %s" % (p.returncode, stderr))
506
507 def __make_scriptlet(self):
508 scriptlet = []
509
510 # Set a different server the update is sent to.
511 server = self.get("server", None)
512 if server:
513 scriptlet.append("server %s" % server)
514
515 # Set the DNS zone the host should be added to.
516 zone = self.get("zone", None)
517 if zone:
518 scriptlet.append("zone %s" % zone)
519
520 key = self.get("key", None)
521 if key:
522 secret = self.get("secret")
523
524 scriptlet.append("key %s %s" % (key, secret))
525
526 ttl = self.get("ttl", self.DEFAULT_TTL)
527
528 # Perform an update for each supported protocol.
529 for rrtype, proto in (("AAAA", "ipv6"), ("A", "ipv4")):
530 address = self.get_address(proto)
531 if not address:
532 continue
533
534 scriptlet.append("update delete %s. %s" % (self.hostname, rrtype))
535 scriptlet.append("update add %s. %s %s %s" % \
536 (self.hostname, ttl, rrtype, address))
537
538 # Send the actions to the server.
539 scriptlet.append("send")
540 scriptlet.append("quit")
541
542 logger.debug(_("Scriptlet:"))
543 for line in scriptlet:
544 # Masquerade the line with the secret key.
545 if line.startswith("key"):
546 line = "key **** ****"
547
548 logger.debug(" %s" % line)
549
550 return "\n".join(scriptlet)
551
552
553 class DDNSProviderChangeIP(DDNSProvider):
554 handle = "changeip.com"
555 name = "ChangeIP.com"
556 website = "https://changeip.com"
557 protocols = ("ipv4",)
558
559 # Detailed information about the update api can be found here.
560 # http://www.changeip.com/accounts/knowledgebase.php?action=displayarticle&id=34
561
562 url = "https://nic.changeip.com/nic/update"
563 can_remove_records = False
564 supports_token_auth = False
565
566 def update_protocol(self, proto):
567 data = {
568 "hostname" : self.hostname,
569 "myip" : self.get_address(proto),
570 }
571
572 # Send update to the server.
573 try:
574 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
575
576 # Handle error codes.
577 except urllib.error.HTTPError as e:
578 if e.code == 422:
579 raise DDNSRequestError(_("Domain not found."))
580
581 raise
582
583 # Handle success message.
584 if response.code == 200:
585 return
586
587 # If we got here, some other update error happened.
588 raise DDNSUpdateError(_("Server response: %s") % output)
589
590
591 class DDNSProviderDesecIO(DDNSProtocolDynDNS2, DDNSProvider):
592 handle = "desec.io"
593 name = "desec.io"
594 website = "https://www.desec.io"
595 protocols = ("ipv6", "ipv4",)
596
597 # ipv4 / ipv6 records are automatically removed when the update
598 # request originates from the respectively other protocol and no
599 # address is explicitly provided for the unused protocol.
600
601 url = "https://update.dedyn.io"
602
603 # desec.io sends the IPv6 and IPv4 address in one request
604
605 def update(self):
606 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
607
608 # This one supports IPv6
609 myipv6 = self.get_address("ipv6")
610
611 # Add update information if we have an IPv6 address.
612 if myipv6:
613 data["myipv6"] = myipv6
614
615 self.send_request(data)
616
617
618 class DDNSProviderDDNSS(DDNSProvider):
619 handle = "ddnss.de"
620 name = "DDNSS"
621 website = "http://www.ddnss.de"
622 protocols = ("ipv4",)
623
624 # Detailed information about how to send the update request and possible response
625 # codes can be obtained from here.
626 # http://www.ddnss.de/info.php
627 # http://www.megacomputing.de/2014/08/dyndns-service-response-time/#more-919
628
629 url = "http://www.ddnss.de/upd.php"
630 can_remove_records = False
631 supports_token_auth = False
632
633 def update_protocol(self, proto):
634 data = {
635 "ip" : self.get_address(proto),
636 "host" : self.hostname,
637 }
638
639 # Check if a token has been set.
640 if self.token:
641 data["key"] = self.token
642
643 # Check if username and hostname are given.
644 elif self.username and self.password:
645 data.update({
646 "user" : self.username,
647 "pwd" : self.password,
648 })
649
650 # Raise an error if no auth details are given.
651 else:
652 raise DDNSConfigurationError
653
654 # Send update to the server.
655 response = self.send_request(self.url, data=data)
656
657 # This provider sends the response code as part of the header.
658 # Get status information from the header.
659 output = response.getheader('ddnss-response')
660
661 # Handle success messages.
662 if output == "good" or output == "nochg":
663 return
664
665 # Handle error codes.
666 if output == "badauth":
667 raise DDNSAuthenticationError
668 elif output == "notfqdn":
669 raise DDNSRequestError(_("No valid FQDN was given"))
670 elif output == "nohost":
671 raise DDNSRequestError(_("Specified host does not exist"))
672 elif output == "911":
673 raise DDNSInternalServerError
674 elif output == "dnserr":
675 raise DDNSInternalServerError(_("DNS error encountered"))
676 elif output == "disabled":
677 raise DDNSRequestError(_("Account disabled or locked"))
678
679 # If we got here, some other update error happened.
680 raise DDNSUpdateError
681
682
683 class DDNSProviderDHS(DDNSProvider):
684 handle = "dhs.org"
685 name = "DHS International"
686 website = "http://dhs.org/"
687 protocols = ("ipv4",)
688
689 # No information about the used update api provided on webpage,
690 # grabed from source code of ez-ipudate.
691
692 url = "http://members.dhs.org/nic/hosts"
693 can_remove_records = False
694 supports_token_auth = False
695
696 def update_protocol(self, proto):
697 data = {
698 "domain" : self.hostname,
699 "ip" : self.get_address(proto),
700 "hostcmd" : "edit",
701 "hostcmdstage" : "2",
702 "type" : "4",
703 }
704
705 # Send update to the server.
706 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
707
708 # Handle success messages.
709 if response.code == 200:
710 return
711
712 # If we got here, some other update error happened.
713 raise DDNSUpdateError
714
715
716 class DDNSProviderDNSpark(DDNSProvider):
717 handle = "dnspark.com"
718 name = "DNS Park"
719 website = "http://dnspark.com/"
720 protocols = ("ipv4",)
721
722 # Informations to the used api can be found here:
723 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
724
725 url = "https://control.dnspark.com/api/dynamic/update.php"
726 can_remove_records = False
727 supports_token_auth = False
728
729 def update_protocol(self, proto):
730 data = {
731 "domain" : self.hostname,
732 "ip" : self.get_address(proto),
733 }
734
735 # Send update to the server.
736 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
737
738 # Get the full response message.
739 output = response.read().decode()
740
741 # Handle success messages.
742 if output.startswith("ok") or output.startswith("nochange"):
743 return
744
745 # Handle error codes.
746 if output == "unauth":
747 raise DDNSAuthenticationError
748 elif output == "abuse":
749 raise DDNSAbuseError
750 elif output == "blocked":
751 raise DDNSBlockedError
752 elif output == "nofqdn":
753 raise DDNSRequestError(_("No valid FQDN was given"))
754 elif output == "nohost":
755 raise DDNSRequestError(_("Invalid hostname specified"))
756 elif output == "notdyn":
757 raise DDNSRequestError(_("Hostname not marked as a dynamic host"))
758 elif output == "invalid":
759 raise DDNSRequestError(_("Invalid IP address has been sent"))
760
761 # If we got here, some other update error happened.
762 raise DDNSUpdateError
763
764
765 class DDNSProviderDtDNS(DDNSProvider):
766 handle = "dtdns.com"
767 name = "DtDNS"
768 website = "http://dtdns.com/"
769 protocols = ("ipv4",)
770
771 # Information about the format of the HTTPS request is to be found
772 # http://www.dtdns.com/dtsite/updatespec
773
774 url = "https://www.dtdns.com/api/autodns.cfm"
775 can_remove_records = False
776 supports_token_auth = False
777
778 def update_protocol(self, proto):
779 data = {
780 "ip" : self.get_address(proto),
781 "id" : self.hostname,
782 "pw" : self.password
783 }
784
785 # Send update to the server.
786 response = self.send_request(self.url, data=data)
787
788 # Get the full response message.
789 output = response.read().decode()
790
791 # Remove all leading and trailing whitespace.
792 output = output.strip()
793
794 # Handle success messages.
795 if "now points to" in output:
796 return
797
798 # Handle error codes.
799 if output == "No hostname to update was supplied.":
800 raise DDNSRequestError(_("No hostname specified"))
801
802 elif output == "The hostname you supplied is not valid.":
803 raise DDNSRequestError(_("Invalid hostname specified"))
804
805 elif output == "The password you supplied is not valid.":
806 raise DDNSAuthenticationError
807
808 elif output == "Administration has disabled this account.":
809 raise DDNSRequestError(_("Account has been disabled"))
810
811 elif output == "Illegal character in IP.":
812 raise DDNSRequestError(_("Invalid IP address has been sent"))
813
814 elif output == "Too many failed requests.":
815 raise DDNSRequestError(_("Too many failed requests"))
816
817 # If we got here, some other update error happened.
818 raise DDNSUpdateError
819
820
821 class DDNSProviderDuckDNS(DDNSProvider):
822 handle = "duckdns.org"
823 name = "Duck DNS"
824 website = "http://www.duckdns.org/"
825 protocols = ("ipv6", "ipv4",)
826
827 # Information about the format of the request is to be found
828 # https://www.duckdns.org/spec.jsp
829
830 url = "https://www.duckdns.org/update"
831 can_remove_records = False
832 supports_token_auth = True
833
834 def update(self):
835 # Raise an error if no auth details are given.
836 if not self.token:
837 raise DDNSConfigurationError
838
839 data = {
840 "domains" : self.hostname,
841 "token" : self.token,
842 }
843
844 # Check if we update an IPv4 address.
845 address4 = self.get_address("ipv4")
846 if address4:
847 data["ip"] = address4
848
849 # Check if we update an IPv6 address.
850 address6 = self.get_address("ipv6")
851 if address6:
852 data["ipv6"] = address6
853
854 # Raise an error if no address is given.
855 if "ip" not in data and "ipv6" not in data:
856 raise DDNSConfigurationError
857
858 # Send update to the server.
859 response = self.send_request(self.url, data=data)
860
861 # Get the full response message.
862 output = response.read().decode()
863
864 # Remove all leading and trailing whitespace.
865 output = output.strip()
866
867 # Handle success messages.
868 if output == "OK":
869 return
870
871 # The provider does not give detailed information
872 # if the update fails. Only a "KO" will be sent back.
873 if output == "KO":
874 raise DDNSUpdateError
875
876 # If we got here, some other update error happened.
877 raise DDNSUpdateError
878
879
880 class DDNSProviderDyFi(DDNSProtocolDynDNS2, DDNSProvider):
881 handle = "dy.fi"
882 name = "dy.fi"
883 website = "https://www.dy.fi/"
884 protocols = ("ipv4",)
885
886 # Information about the format of the request is to be found
887 # https://www.dy.fi/page/clients?lang=en
888 # https://www.dy.fi/page/specification?lang=en
889
890 url = "https://www.dy.fi/nic/update"
891
892 # Please only send automatic updates when your IP address changes,
893 # or once per 5 to 6 days to refresh the address mapping (they will
894 # expire if not refreshed within 7 days).
895 holdoff_days = 6
896
897
898 class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
899 handle = "dyndns.org"
900 name = "Dyn"
901 website = "http://dyn.com/dns/"
902 protocols = ("ipv4",)
903
904 # Information about the format of the request is to be found
905 # http://http://dyn.com/support/developers/api/perform-update/
906 # http://dyn.com/support/developers/api/return-codes/
907
908 url = "https://members.dyndns.org/nic/update"
909
910
911 class DDNSProviderDomainOffensive(DDNSProtocolDynDNS2, DDNSProvider):
912 handle = "do.de"
913 name = "Domain-Offensive"
914 website = "https://www.do.de/"
915 protocols = ("ipv6", "ipv4")
916
917 # Detailed information about the request and response codes
918 # are available on the providers webpage.
919 # https://www.do.de/wiki/FlexDNS_-_Entwickler
920
921 url = "https://ddns.do.de/"
922
923 class DDNSProviderDynUp(DDNSProvider):
924 handle = "dynup.de"
925 name = "DynUp.DE"
926 website = "http://dynup.de/"
927 protocols = ("ipv4",)
928
929 # Information about the format of the HTTPS request is to be found
930 # https://dyndnsfree.de/user/hilfe.php
931
932 url = "https://dynup.de/dyn.php"
933 can_remove_records = False
934 supports_token_auth = False
935
936 def update_protocol(self, proto):
937 data = {
938 "username" : self.username,
939 "password" : self.password,
940 "hostname" : self.hostname,
941 "print" : '1',
942 }
943
944 # Send update to the server.
945 response = self.send_request(self.url, data=data)
946
947 # Get the full response message.
948 output = response.read().decode()
949
950 # Remove all leading and trailing whitespace.
951 output = output.strip()
952
953 # Handle success messages.
954 if output.startswith("I:OK"):
955 return
956
957 # If we got here, some other update error happened.
958 raise DDNSUpdateError
959
960
961 class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
962 handle = "dynu.com"
963 name = "Dynu"
964 website = "http://dynu.com/"
965 protocols = ("ipv6", "ipv4",)
966
967 # Detailed information about the request and response codes
968 # are available on the providers webpage.
969 # http://dynu.com/Default.aspx?page=dnsapi
970
971 url = "https://api.dynu.com/nic/update"
972
973 # DynU sends the IPv6 and IPv4 address in one request
974
975 def update(self):
976 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
977
978 # This one supports IPv6
979 myipv6 = self.get_address("ipv6")
980
981 # Add update information if we have an IPv6 address.
982 if myipv6:
983 data["myipv6"] = myipv6
984
985 self.send_request(data)
986
987
988 class DDNSProviderEasyDNS(DDNSProvider):
989 handle = "easydns.com"
990 name = "EasyDNS"
991 website = "http://www.easydns.com/"
992 protocols = ("ipv4",)
993
994 # Detailed information about the request and response codes
995 # (API 1.3) are available on the providers webpage.
996 # https://fusion.easydns.com/index.php?/Knowledgebase/Article/View/102/7/dynamic-dns
997
998 url = "http://api.cp.easydns.com/dyn/tomato.php"
999
1000 supports_token_auth = False
1001
1002 def update_protocol(self, proto):
1003 data = {
1004 "myip" : self.get_address(proto, "-"),
1005 "hostname" : self.hostname,
1006 }
1007
1008 # Send update to the server.
1009 response = self.send_request(self.url, data=data, username=self.username, password=self.password)
1010
1011 # Get the full response message.
1012 output = response.read().decode()
1013
1014 # Remove all leading and trailing whitespace.
1015 output = output.strip()
1016
1017 # Handle success messages.
1018 if output.startswith("NOERROR"):
1019 return
1020
1021 # Handle error codes.
1022 if output.startswith("NOACCESS"):
1023 raise DDNSAuthenticationError
1024
1025 elif output.startswith("NOSERVICE"):
1026 raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain"))
1027
1028 elif output.startswith("ILLEGAL INPUT"):
1029 raise DDNSRequestError(_("Invalid data has been sent"))
1030
1031 elif output.startswith("TOOSOON"):
1032 raise DDNSRequestError(_("Too frequent update requests have been sent"))
1033
1034 # If we got here, some other update error happened.
1035 raise DDNSUpdateError
1036
1037
1038 class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
1039 handle = "domopoli.de"
1040 name = "domopoli.de"
1041 website = "http://domopoli.de/"
1042 protocols = ("ipv4",)
1043
1044 # https://www.domopoli.de/?page=howto#DynDns_start
1045
1046 url = "http://dyndns.domopoli.de/nic/update"
1047
1048
1049 class DDNSProviderDynsNet(DDNSProvider):
1050 handle = "dyns.net"
1051 name = "DyNS"
1052 website = "http://www.dyns.net/"
1053 protocols = ("ipv4",)
1054 can_remove_records = False
1055 supports_token_auth = False
1056
1057 # There is very detailed informatio about how to send the update request and
1058 # the possible response codes. (Currently we are using the v1.1 proto)
1059 # http://www.dyns.net/documentation/technical/protocol/
1060
1061 url = "http://www.dyns.net/postscript011.php"
1062
1063 def update_protocol(self, proto):
1064 data = {
1065 "ip" : self.get_address(proto),
1066 "host" : self.hostname,
1067 "username" : self.username,
1068 "password" : self.password,
1069 }
1070
1071 # Send update to the server.
1072 response = self.send_request(self.url, data=data)
1073
1074 # Get the full response message.
1075 output = response.read().decode()
1076
1077 # Handle success messages.
1078 if output.startswith("200"):
1079 return
1080
1081 # Handle error codes.
1082 if output.startswith("400"):
1083 raise DDNSRequestError(_("Malformed request has been sent"))
1084 elif output.startswith("401"):
1085 raise DDNSAuthenticationError
1086 elif output.startswith("402"):
1087 raise DDNSRequestError(_("Too frequent update requests have been sent"))
1088 elif output.startswith("403"):
1089 raise DDNSInternalServerError
1090
1091 # If we got here, some other update error happened.
1092 raise DDNSUpdateError(_("Server response: %s") % output)
1093
1094
1095 class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
1096 handle = "enom.com"
1097 name = "eNom Inc."
1098 website = "http://www.enom.com/"
1099 protocols = ("ipv4",)
1100
1101 # There are very detailed information about how to send an update request and
1102 # the respone codes.
1103 # http://www.enom.com/APICommandCatalog/
1104
1105 url = "https://dynamic.name-services.com/interface.asp"
1106 can_remove_records = False
1107 supports_token_auth = False
1108
1109 def update_protocol(self, proto):
1110 data = {
1111 "command" : "setdnshost",
1112 "responsetype" : "xml",
1113 "address" : self.get_address(proto),
1114 "domainpassword" : self.password,
1115 "zone" : self.hostname
1116 }
1117
1118 # Send update to the server.
1119 response = self.send_request(self.url, data=data)
1120
1121 # Get the full response message.
1122 output = response.read().decode()
1123
1124 # Handle success messages.
1125 if self.get_xml_tag_value(output, "ErrCount") == "0":
1126 return
1127
1128 # Handle error codes.
1129 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
1130
1131 if errorcode == "304155":
1132 raise DDNSAuthenticationError
1133 elif errorcode == "304153":
1134 raise DDNSRequestError(_("Domain not found"))
1135
1136 # If we got here, some other update error happened.
1137 raise DDNSUpdateError
1138
1139
1140 class DDNSProviderEntryDNS(DDNSProvider):
1141 handle = "entrydns.net"
1142 name = "EntryDNS"
1143 website = "http://entrydns.net/"
1144 protocols = ("ipv4",)
1145
1146 # Some very tiny details about their so called "Simple API" can be found
1147 # here: https://entrydns.net/help
1148 url = "https://entrydns.net/records/modify"
1149 can_remove_records = False
1150 supports_token_auth = True
1151
1152 def update_protocol(self, proto):
1153 data = {
1154 "ip" : self.get_address(proto),
1155 }
1156
1157 # Add auth token to the update url.
1158 url = "%s/%s" % (self.url, self.token)
1159
1160 # Send update to the server.
1161 try:
1162 response = self.send_request(url, data=data)
1163
1164 # Handle error codes
1165 except urllib.error.HTTPError as e:
1166 if e.code == 404:
1167 raise DDNSAuthenticationError
1168
1169 elif e.code == 422:
1170 raise DDNSRequestError(_("An invalid IP address was submitted"))
1171
1172 raise
1173
1174 # Handle success messages.
1175 if response.code == 200:
1176 return
1177
1178 # If we got here, some other update error happened.
1179 raise DDNSUpdateError
1180
1181
1182 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
1183 handle = "freedns.afraid.org"
1184 name = "freedns.afraid.org"
1185 website = "http://freedns.afraid.org/"
1186
1187 # No information about the request or response could be found on the vendor
1188 # page. All used values have been collected by testing.
1189 url = "https://freedns.afraid.org/dynamic/update.php"
1190 can_remove_records = False
1191 supports_token_auth = True
1192
1193 def update_protocol(self, proto):
1194 data = {
1195 "address" : self.get_address(proto),
1196 }
1197
1198 # Add auth token to the update url.
1199 url = "%s?%s" % (self.url, self.token)
1200
1201 # Send update to the server.
1202 response = self.send_request(url, data=data)
1203
1204 # Get the full response message.
1205 output = response.read().decode()
1206
1207 # Handle success messages.
1208 if output.startswith("Updated") or "has not changed" in output:
1209 return
1210
1211 # Handle error codes.
1212 if output == "ERROR: Unable to locate this record":
1213 raise DDNSAuthenticationError
1214 elif "is an invalid IP address" in output:
1215 raise DDNSRequestError(_("Invalid IP address has been sent"))
1216
1217 # If we got here, some other update error happened.
1218 raise DDNSUpdateError
1219
1220
1221 class DDNSProviderItsdns(DDNSProtocolDynDNS2, DDNSProvider):
1222 handle = "inwx.com"
1223 name = "INWX"
1224 website = "https://www.inwx.com"
1225 protocols = ("ipv6", "ipv4")
1226
1227 # Information about the format of the HTTP request is to be found
1228 # here: https://www.inwx.com/en/nameserver2/dyndns (requires login)
1229 # Notice: The URL is the same for: inwx.com|de|at|ch|es
1230
1231 url = "https://dyndns.inwx.com/nic/update"
1232
1233
1234 class DDNSProviderItsdns(DDNSProtocolDynDNS2, DDNSProvider):
1235 handle = "itsdns.de"
1236 name = "it's DNS"
1237 website = "http://www.itsdns.de/"
1238 protocols = ("ipv6", "ipv4")
1239
1240 # Information about the format of the HTTP request is to be found
1241 # here: https://www.itsdns.de/dynupdatehelp.htm
1242
1243 url = "https://www.itsdns.de/update.php"
1244
1245
1246 class DDNSProviderJoker(DDNSProtocolDynDNS2, DDNSProvider):
1247 handle = "joker.com"
1248 name = "Joker.com Dynamic DNS"
1249 website = "https://joker.com/"
1250 protocols = ("ipv4",)
1251
1252 # Information about the request can be found here:
1253 # https://joker.com/faq/content/11/427/en/what-is-dynamic-dns-dyndns.html
1254 # Using DynDNS V2 protocol over HTTPS here
1255
1256 url = "https://svc.joker.com/nic/update"
1257
1258
1259 class DDNSProviderKEYSYSTEMS(DDNSProvider):
1260 handle = "key-systems.net"
1261 name = "dynamicdns.key-systems.net"
1262 website = "https://domaindiscount24.com/"
1263 protocols = ("ipv4",)
1264
1265 # There are only information provided by the domaindiscount24 how to
1266 # perform an update with HTTP APIs
1267 # https://www.domaindiscount24.com/faq/dynamic-dns
1268 # examples: https://dynamicdns.key-systems.net/update.php?hostname=hostname&password=password&ip=auto
1269 # https://dynamicdns.key-systems.net/update.php?hostname=hostname&password=password&ip=213.x.x.x&mx=213.x.x.x
1270
1271 url = "https://dynamicdns.key-systems.net/update.php"
1272 can_remove_records = False
1273 supports_token_auth = False
1274
1275 def update_protocol(self, proto):
1276 address = self.get_address(proto)
1277 data = {
1278 "hostname" : self.hostname,
1279 "password" : self.password,
1280 "ip" : address,
1281 }
1282
1283 # Send update to the server.
1284 response = self.send_request(self.url, data=data)
1285
1286 # Get the full response message.
1287 output = response.read().decode()
1288
1289 # Handle success messages.
1290 if "code = 200" in output:
1291 return
1292
1293 # Handle error messages.
1294 if "abuse prevention triggered" in output:
1295 raise DDNSAbuseError
1296 elif "invalid password" in output:
1297 raise DDNSAuthenticationError
1298 elif "Authorization failed" in output:
1299 raise DDNSRequestError(_("Invalid hostname specified"))
1300
1301 # If we got here, some other update error happened.
1302 raise DDNSUpdateError
1303
1304
1305 class DDNSProviderGoogle(DDNSProtocolDynDNS2, DDNSProvider):
1306 handle = "domains.google.com"
1307 name = "Google Domains"
1308 website = "https://domains.google.com/"
1309 protocols = ("ipv4",)
1310
1311 # Information about the format of the HTTP request is to be found
1312 # here: https://support.google.com/domains/answer/6147083?hl=en
1313
1314 url = "https://domains.google.com/nic/update"
1315
1316
1317 class DDNSProviderLightningWireLabs(DDNSProvider):
1318 handle = "dns.lightningwirelabs.com"
1319 name = "Lightning Wire Labs DNS Service"
1320 website = "https://dns.lightningwirelabs.com/"
1321
1322 # Information about the format of the HTTPS request is to be found
1323 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
1324
1325 supports_token_auth = True
1326
1327 url = "https://dns.lightningwirelabs.com/update"
1328
1329 def update(self):
1330 # Raise an error if no auth details are given.
1331 if not self.token:
1332 raise DDNSConfigurationError
1333
1334 data = {
1335 "hostname" : self.hostname,
1336 "token" : self.token,
1337 "address6" : self.get_address("ipv6", "-"),
1338 "address4" : self.get_address("ipv4", "-"),
1339 }
1340
1341 # Send update to the server.
1342 response = self.send_request(self.url, data=data)
1343
1344 # Handle success messages.
1345 if response.code == 200:
1346 return
1347
1348 # If we got here, some other update error happened.
1349 raise DDNSUpdateError
1350
1351
1352 class DDNSProviderLoopia(DDNSProtocolDynDNS2, DDNSProvider):
1353 handle = "loopia.se"
1354 name = "Loopia AB"
1355 website = "https://www.loopia.com"
1356 protocols = ("ipv4",)
1357
1358 # Information about the format of the HTTP request is to be found
1359 # here: https://support.loopia.com/wiki/About_the_DynDNS_support
1360
1361 url = "https://dns.loopia.se/XDynDNSServer/XDynDNS.php"
1362
1363
1364 class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
1365 handle = "myonlineportal.net"
1366 name = "myonlineportal.net"
1367 website = "https:/myonlineportal.net/"
1368
1369 # Information about the request and response can be obtained here:
1370 # https://myonlineportal.net/howto_dyndns
1371
1372 url = "https://myonlineportal.net/updateddns"
1373
1374 def prepare_request_data(self, proto):
1375 data = {
1376 "hostname" : self.hostname,
1377 "ip" : self.get_address(proto),
1378 }
1379
1380 return data
1381
1382
1383 class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
1384 handle = "namecheap.com"
1385 name = "Namecheap"
1386 website = "http://namecheap.com"
1387 protocols = ("ipv4",)
1388
1389 # Information about the format of the HTTP request is to be found
1390 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
1391 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
1392
1393 url = "https://dynamicdns.park-your-domain.com/update"
1394 can_remove_records = False
1395 supports_token_auth = False
1396
1397 def update_protocol(self, proto):
1398 # Namecheap requires the hostname splitted into a host and domain part.
1399 host, domain = self.hostname.split(".", 1)
1400
1401 # Get and store curent IP address.
1402 address = self.get_address(proto)
1403
1404 data = {
1405 "ip" : address,
1406 "password" : self.password,
1407 "host" : host,
1408 "domain" : domain
1409 }
1410
1411 # Send update to the server.
1412 response = self.send_request(self.url, data=data)
1413
1414 # Get the full response message.
1415 output = response.read().decode()
1416
1417 # Handle success messages.
1418 if self.get_xml_tag_value(output, "IP") == address:
1419 return
1420
1421 # Handle error codes.
1422 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
1423
1424 if errorcode == "304156":
1425 raise DDNSAuthenticationError
1426 elif errorcode == "316153":
1427 raise DDNSRequestError(_("Domain not found"))
1428 elif errorcode == "316154":
1429 raise DDNSRequestError(_("Domain not active"))
1430 elif errorcode in ("380098", "380099"):
1431 raise DDNSInternalServerError
1432
1433 # If we got here, some other update error happened.
1434 raise DDNSUpdateError
1435
1436
1437 class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
1438 handle = "no-ip.com"
1439 name = "NoIP"
1440 website = "http://www.noip.com/"
1441 protocols = ("ipv4",)
1442
1443 # Information about the format of the HTTP request is to be found
1444 # here: http://www.noip.com/integrate/request and
1445 # here: http://www.noip.com/integrate/response
1446
1447 url = "http://dynupdate.noip.com/nic/update"
1448
1449 def prepare_request_data(self, proto):
1450 assert proto == "ipv4"
1451
1452 data = {
1453 "hostname" : self.hostname,
1454 "address" : self.get_address(proto),
1455 }
1456
1457 return data
1458
1459
1460 class DDNSProviderNowDNS(DDNSProtocolDynDNS2, DDNSProvider):
1461 handle = "now-dns.com"
1462 name = "NOW-DNS"
1463 website = "http://now-dns.com/"
1464 protocols = ("ipv6", "ipv4")
1465
1466 # Information about the format of the request is to be found
1467 # but only can be accessed by register an account and login
1468 # https://now-dns.com/?m=api
1469
1470 url = "https://now-dns.com/update"
1471
1472
1473 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
1474 handle = "nsupdate.info"
1475 name = "nsupdate.info"
1476 website = "http://nsupdate.info/"
1477 protocols = ("ipv6", "ipv4",)
1478
1479 # Information about the format of the HTTP request can be found
1480 # after login on the provider user interface and here:
1481 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1482
1483 url = "https://nsupdate.info/nic/update"
1484
1485 # TODO nsupdate.info can actually do this, but the functionality
1486 # has not been implemented here, yet.
1487 can_remove_records = False
1488
1489 supports_token_auth = True
1490
1491 # After a failed update, there will be no retries
1492 # https://bugzilla.ipfire.org/show_bug.cgi?id=10603
1493 holdoff_failure_days = None
1494
1495 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1496 # and for the password a so called secret.
1497 @property
1498 def username(self):
1499 return self.get("hostname")
1500
1501 @property
1502 def password(self):
1503 return self.token or self.get("secret")
1504
1505 def prepare_request_data(self, proto):
1506 data = {
1507 "myip" : self.get_address(proto),
1508 }
1509
1510 return data
1511
1512
1513 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
1514 handle = "opendns.com"
1515 name = "OpenDNS"
1516 website = "http://www.opendns.com"
1517
1518 # Detailed information about the update request and possible
1519 # response codes can be obtained from here:
1520 # https://support.opendns.com/entries/23891440
1521
1522 url = "https://updates.opendns.com/nic/update"
1523
1524 def prepare_request_data(self, proto):
1525 data = {
1526 "hostname" : self.hostname,
1527 "myip" : self.get_address(proto),
1528 }
1529
1530 return data
1531
1532
1533 class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1534 handle = "ovh.com"
1535 name = "OVH"
1536 website = "http://www.ovh.com/"
1537 protocols = ("ipv4",)
1538
1539 # OVH only provides very limited information about how to
1540 # update a DynDNS host. They only provide the update url
1541 # on the their german subpage.
1542 #
1543 # http://hilfe.ovh.de/DomainDynHost
1544
1545 url = "https://www.ovh.com/nic/update"
1546
1547 def prepare_request_data(self, proto):
1548 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1549 data.update({
1550 "system" : "dyndns",
1551 })
1552
1553 return data
1554
1555
1556 class DDNSProviderRegfish(DDNSProvider):
1557 handle = "regfish.com"
1558 name = "Regfish GmbH"
1559 website = "http://www.regfish.com/"
1560
1561 # A full documentation to the providers api can be found here
1562 # but is only available in german.
1563 # https://www.regfish.de/domains/dyndns/dokumentation
1564
1565 url = "https://dyndns.regfish.de/"
1566 can_remove_records = False
1567 supports_token_auth = True
1568
1569 def update(self):
1570 data = {
1571 "fqdn" : self.hostname,
1572 }
1573
1574 # Check if we update an IPv6 address.
1575 address6 = self.get_address("ipv6")
1576 if address6:
1577 data["ipv6"] = address6
1578
1579 # Check if we update an IPv4 address.
1580 address4 = self.get_address("ipv4")
1581 if address4:
1582 data["ipv4"] = address4
1583
1584 # Raise an error if none address is given.
1585 if "ipv6" not in data and "ipv4" not in data:
1586 raise DDNSConfigurationError
1587
1588 # Check if a token has been set.
1589 if self.token:
1590 data["token"] = self.token
1591
1592 # Raise an error if no token and no useranem and password
1593 # are given.
1594 elif not self.username and not self.password:
1595 raise DDNSConfigurationError(_("No Auth details specified"))
1596
1597 # HTTP Basic Auth is only allowed if no token is used.
1598 if self.token:
1599 # Send update to the server.
1600 response = self.send_request(self.url, data=data)
1601 else:
1602 # Send update to the server.
1603 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
1604
1605 # Get the full response message.
1606 output = response.read().decode()
1607
1608 # Handle success messages.
1609 if "100" in output or "101" in output:
1610 return
1611
1612 # Handle error codes.
1613 if "401" or "402" in output:
1614 raise DDNSAuthenticationError
1615 elif "408" in output:
1616 raise DDNSRequestError(_("Invalid IPv4 address has been sent"))
1617 elif "409" in output:
1618 raise DDNSRequestError(_("Invalid IPv6 address has been sent"))
1619 elif "412" in output:
1620 raise DDNSRequestError(_("No valid FQDN was given"))
1621 elif "414" in output:
1622 raise DDNSInternalServerError
1623
1624 # If we got here, some other update error happened.
1625 raise DDNSUpdateError
1626
1627
1628 class DDNSProviderSchokokeksDNS(DDNSProtocolDynDNS2, DDNSProvider):
1629 handle = "schokokeks.org"
1630 name = "Schokokeks"
1631 website = "http://www.schokokeks.org/"
1632 protocols = ("ipv4",)
1633
1634 # Information about the format of the request is to be found
1635 # https://wiki.schokokeks.org/DynDNS
1636 url = "https://dyndns.schokokeks.org/nic/update"
1637
1638
1639 class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
1640 handle = "selfhost.de"
1641 name = "Selfhost.de"
1642 website = "http://www.selfhost.de/"
1643 protocols = ("ipv4",)
1644
1645 url = "https://carol.selfhost.de/nic/update"
1646
1647 def prepare_request_data(self, proto):
1648 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1649 data.update({
1650 "hostname" : "1",
1651 })
1652
1653 return data
1654
1655
1656 class DDNSProviderServercow(DDNSProvider):
1657 handle = "servercow.de"
1658 name = "servercow.de"
1659 website = "https://servercow.de/"
1660 protocols = ("ipv4", "ipv6")
1661
1662 url = "https://www.servercow.de/dnsupdate/update.php"
1663 can_remove_records = False
1664 supports_token_auth = False
1665
1666 def update_protocol(self, proto):
1667 data = {
1668 "ipaddr" : self.get_address(proto),
1669 "hostname" : self.hostname,
1670 "username" : self.username,
1671 "pass" : self.password,
1672 }
1673
1674 # Send request to provider
1675 response = self.send_request(self.url, data=data)
1676
1677 # Read response
1678 output = response.read().decode()
1679
1680 # Server responds with OK if update was successful
1681 if output.startswith("OK"):
1682 return
1683
1684 # Catch any errors
1685 elif output.startswith("FAILED - Authentication failed"):
1686 raise DDNSAuthenticationError
1687
1688 # If we got here, some other update error happened
1689 raise DDNSUpdateError(output)
1690
1691
1692 class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1693 handle = "spdns.org"
1694 name = "SPDYN"
1695 website = "https://www.spdyn.de/"
1696
1697 # Detailed information about request and response codes are provided
1698 # by the vendor. They are using almost the same mechanism and status
1699 # codes as dyndns.org so we can inherit all those stuff.
1700 #
1701 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1702 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1703
1704 url = "https://update.spdyn.de/nic/update"
1705
1706 supports_token_auth = True
1707
1708 @property
1709 def username(self):
1710 return self.get("username") or self.hostname
1711
1712 @property
1713 def password(self):
1714 return self.get("password") or self.token
1715
1716
1717 class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1718 handle = "strato.com"
1719 name = "Strato AG"
1720 website = "http:/www.strato.com/"
1721 protocols = ("ipv4",)
1722
1723 # Information about the request and response can be obtained here:
1724 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1725
1726 url = "https://dyndns.strato.com/nic/update"
1727
1728 def prepare_request_data(self, proto):
1729 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1730 data.update({
1731 "mx" : "NOCHG",
1732 "backupmx" : "NOCHG"
1733 })
1734
1735 return data
1736
1737
1738 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1739 handle = "twodns.de"
1740 name = "TwoDNS"
1741 website = "http://www.twodns.de"
1742 protocols = ("ipv4",)
1743
1744 # Detailed information about the request can be found here
1745 # http://twodns.de/en/faqs
1746 # http://twodns.de/en/api
1747
1748 url = "https://update.twodns.de/update"
1749
1750 def prepare_request_data(self, proto):
1751 assert proto == "ipv4"
1752
1753 data = {
1754 "ip" : self.get_address(proto),
1755 "hostname" : self.hostname
1756 }
1757
1758 return data
1759
1760
1761 class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1762 handle = "udmedia.de"
1763 name = "Udmedia GmbH"
1764 website = "http://www.udmedia.de"
1765 protocols = ("ipv4",)
1766
1767 # Information about the request can be found here
1768 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1769
1770 url = "https://www.udmedia.de/nic/update"
1771
1772
1773 class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
1774 handle = "variomedia.de"
1775 name = "Variomedia"
1776 website = "http://www.variomedia.de/"
1777 protocols = ("ipv6", "ipv4",)
1778
1779 # Detailed information about the request can be found here
1780 # https://dyndns.variomedia.de/
1781
1782 url = "https://dyndns.variomedia.de/nic/update"
1783
1784 def prepare_request_data(self, proto):
1785 data = {
1786 "hostname" : self.hostname,
1787 "myip" : self.get_address(proto),
1788 }
1789
1790 return data
1791
1792
1793 class DDNSProviderXLhost(DDNSProtocolDynDNS2, DDNSProvider):
1794 handle = "xlhost.de"
1795 name = "XLhost"
1796 website = "http://xlhost.de/"
1797 protocols = ("ipv4",)
1798
1799 # Information about the format of the HTTP request is to be found
1800 # here: https://xlhost.de/faq/index_html?topicId=CQA2ELIPO4SQ
1801
1802 url = "https://nsupdate.xlhost.de/"
1803
1804
1805 class DDNSProviderZoneedit(DDNSProvider):
1806 handle = "zoneedit.com"
1807 name = "Zoneedit"
1808 website = "http://www.zoneedit.com"
1809 protocols = ("ipv4",)
1810
1811 supports_token_auth = False
1812
1813 # Detailed information about the request and the response codes can be
1814 # obtained here:
1815 # http://www.zoneedit.com/doc/api/other.html
1816 # http://www.zoneedit.com/faq.html
1817
1818 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1819
1820 def update_protocol(self, proto):
1821 data = {
1822 "dnsto" : self.get_address(proto),
1823 "host" : self.hostname
1824 }
1825
1826 # Send update to the server.
1827 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
1828
1829 # Get the full response message.
1830 output = response.read().decode()
1831
1832 # Handle success messages.
1833 if output.startswith("<SUCCESS"):
1834 return
1835
1836 # Handle error codes.
1837 if output.startswith("invalid login"):
1838 raise DDNSAuthenticationError
1839 elif output.startswith("<ERROR CODE=\"704\""):
1840 raise DDNSRequestError(_("No valid FQDN was given"))
1841 elif output.startswith("<ERROR CODE=\"702\""):
1842 raise DDNSRequestError(_("Too frequent update requests have been sent"))
1843
1844 # If we got here, some other update error happened.
1845 raise DDNSUpdateError
1846
1847
1848 class DDNSProviderDNSmadeEasy(DDNSProvider):
1849 handle = "dnsmadeeasy.com"
1850 name = "DNSmadeEasy.com"
1851 website = "http://www.dnsmadeeasy.com/"
1852 protocols = ("ipv4",)
1853
1854 # DNS Made Easy Nameserver Provider also offering Dynamic DNS
1855 # Documentation can be found here:
1856 # http://www.dnsmadeeasy.com/dynamic-dns/
1857
1858 url = "https://cp.dnsmadeeasy.com/servlet/updateip?"
1859 can_remove_records = False
1860 supports_token_auth = False
1861
1862 def update_protocol(self, proto):
1863 data = {
1864 "ip" : self.get_address(proto),
1865 "id" : self.hostname,
1866 "username" : self.username,
1867 "password" : self.password,
1868 }
1869
1870 # Send update to the server.
1871 response = self.send_request(self.url, data=data)
1872
1873 # Get the full response message.
1874 output = response.read().decode()
1875
1876 # Handle success messages.
1877 if output.startswith("success") or output.startswith("error-record-ip-same"):
1878 return
1879
1880 # Handle error codes.
1881 if output.startswith("error-auth-suspend"):
1882 raise DDNSRequestError(_("Account has been suspended"))
1883
1884 elif output.startswith("error-auth-voided"):
1885 raise DDNSRequestError(_("Account has been revoked"))
1886
1887 elif output.startswith("error-record-invalid"):
1888 raise DDNSRequestError(_("Specified host does not exist"))
1889
1890 elif output.startswith("error-auth"):
1891 raise DDNSAuthenticationError
1892
1893 # If we got here, some other update error happened.
1894 raise DDNSUpdateError(_("Server response: %s") % output)
1895
1896
1897 class DDNSProviderZZZZ(DDNSProvider):
1898 handle = "zzzz.io"
1899 name = "zzzz"
1900 website = "https://zzzz.io"
1901 protocols = ("ipv6", "ipv4",)
1902
1903 # Detailed information about the update request can be found here:
1904 # https://zzzz.io/faq/
1905
1906 # Details about the possible response codes have been provided in the bugtracker:
1907 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1908
1909 url = "https://zzzz.io/api/v1/update"
1910 can_remove_records = False
1911 supports_token_auth = True
1912
1913 def update_protocol(self, proto):
1914 data = {
1915 "ip" : self.get_address(proto),
1916 "token" : self.token,
1917 }
1918
1919 if proto == "ipv6":
1920 data["type"] = "aaaa"
1921
1922 # zzzz uses the host from the full hostname as part
1923 # of the update url.
1924 host, domain = self.hostname.split(".", 1)
1925
1926 # Add host value to the update url.
1927 url = "%s/%s" % (self.url, host)
1928
1929 # Send update to the server.
1930 try:
1931 response = self.send_request(url, data=data)
1932
1933 # Handle error codes.
1934 except DDNSNotFound:
1935 raise DDNSRequestError(_("Invalid hostname specified"))
1936
1937 # Handle success messages.
1938 if response.code == 200:
1939 return
1940
1941 # If we got here, some other update error happened.
1942 raise DDNSUpdateError