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