]> git.ipfire.org Git - oddments/ddns.git/blob - src/ddns/providers.py
easydns.com: Send request via HTTPS
[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 = "https://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).encode()
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 = "https://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 # Provider currently does not support TLS 1.2.
693 url = "https://members.dhs.org/nic/hosts"
694 can_remove_records = False
695 supports_token_auth = False
696
697 def update_protocol(self, proto):
698 data = {
699 "domain" : self.hostname,
700 "ip" : self.get_address(proto),
701 "hostcmd" : "edit",
702 "hostcmdstage" : "2",
703 "type" : "4",
704 }
705
706 # Send update to the server.
707 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
708
709 # Handle success messages.
710 if response.code == 200:
711 return
712
713 # If we got here, some other update error happened.
714 raise DDNSUpdateError
715
716
717 class DDNSProviderDNSpark(DDNSProvider):
718 handle = "dnspark.com"
719 name = "DNS Park"
720 website = "http://dnspark.com/"
721 protocols = ("ipv4",)
722
723 # Informations to the used api can be found here:
724 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
725
726 url = "https://control.dnspark.com/api/dynamic/update.php"
727 can_remove_records = False
728 supports_token_auth = False
729
730 def update_protocol(self, proto):
731 data = {
732 "domain" : self.hostname,
733 "ip" : self.get_address(proto),
734 }
735
736 # Send update to the server.
737 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
738
739 # Get the full response message.
740 output = response.read().decode()
741
742 # Handle success messages.
743 if output.startswith("ok") or output.startswith("nochange"):
744 return
745
746 # Handle error codes.
747 if output == "unauth":
748 raise DDNSAuthenticationError
749 elif output == "abuse":
750 raise DDNSAbuseError
751 elif output == "blocked":
752 raise DDNSBlockedError
753 elif output == "nofqdn":
754 raise DDNSRequestError(_("No valid FQDN was given"))
755 elif output == "nohost":
756 raise DDNSRequestError(_("Invalid hostname specified"))
757 elif output == "notdyn":
758 raise DDNSRequestError(_("Hostname not marked as a dynamic host"))
759 elif output == "invalid":
760 raise DDNSRequestError(_("Invalid IP address has been sent"))
761
762 # If we got here, some other update error happened.
763 raise DDNSUpdateError
764
765
766 class DDNSProviderDtDNS(DDNSProvider):
767 handle = "dtdns.com"
768 name = "DtDNS"
769 website = "http://dtdns.com/"
770 protocols = ("ipv4",)
771
772 # Information about the format of the HTTPS request is to be found
773 # http://www.dtdns.com/dtsite/updatespec
774
775 url = "https://www.dtdns.com/api/autodns.cfm"
776 can_remove_records = False
777 supports_token_auth = False
778
779 def update_protocol(self, proto):
780 data = {
781 "ip" : self.get_address(proto),
782 "id" : self.hostname,
783 "pw" : self.password
784 }
785
786 # Send update to the server.
787 response = self.send_request(self.url, data=data)
788
789 # Get the full response message.
790 output = response.read().decode()
791
792 # Remove all leading and trailing whitespace.
793 output = output.strip()
794
795 # Handle success messages.
796 if "now points to" in output:
797 return
798
799 # Handle error codes.
800 if output == "No hostname to update was supplied.":
801 raise DDNSRequestError(_("No hostname specified"))
802
803 elif output == "The hostname you supplied is not valid.":
804 raise DDNSRequestError(_("Invalid hostname specified"))
805
806 elif output == "The password you supplied is not valid.":
807 raise DDNSAuthenticationError
808
809 elif output == "Administration has disabled this account.":
810 raise DDNSRequestError(_("Account has been disabled"))
811
812 elif output == "Illegal character in IP.":
813 raise DDNSRequestError(_("Invalid IP address has been sent"))
814
815 elif output == "Too many failed requests.":
816 raise DDNSRequestError(_("Too many failed requests"))
817
818 # If we got here, some other update error happened.
819 raise DDNSUpdateError
820
821
822 class DDNSProviderDuckDNS(DDNSProvider):
823 handle = "duckdns.org"
824 name = "Duck DNS"
825 website = "http://www.duckdns.org/"
826 protocols = ("ipv6", "ipv4",)
827
828 # Information about the format of the request is to be found
829 # https://www.duckdns.org/spec.jsp
830
831 url = "https://www.duckdns.org/update"
832 can_remove_records = False
833 supports_token_auth = True
834
835 def update(self):
836 # Raise an error if no auth details are given.
837 if not self.token:
838 raise DDNSConfigurationError
839
840 data = {
841 "domains" : self.hostname,
842 "token" : self.token,
843 }
844
845 # Check if we update an IPv4 address.
846 address4 = self.get_address("ipv4")
847 if address4:
848 data["ip"] = address4
849
850 # Check if we update an IPv6 address.
851 address6 = self.get_address("ipv6")
852 if address6:
853 data["ipv6"] = address6
854
855 # Raise an error if no address is given.
856 if "ip" not in data and "ipv6" not in data:
857 raise DDNSConfigurationError
858
859 # Send update to the server.
860 response = self.send_request(self.url, data=data)
861
862 # Get the full response message.
863 output = response.read().decode()
864
865 # Remove all leading and trailing whitespace.
866 output = output.strip()
867
868 # Handle success messages.
869 if output == "OK":
870 return
871
872 # The provider does not give detailed information
873 # if the update fails. Only a "KO" will be sent back.
874 if output == "KO":
875 raise DDNSUpdateError
876
877 # If we got here, some other update error happened.
878 raise DDNSUpdateError
879
880
881 class DDNSProviderDyFi(DDNSProtocolDynDNS2, DDNSProvider):
882 handle = "dy.fi"
883 name = "dy.fi"
884 website = "https://www.dy.fi/"
885 protocols = ("ipv4",)
886
887 # Information about the format of the request is to be found
888 # https://www.dy.fi/page/clients?lang=en
889 # https://www.dy.fi/page/specification?lang=en
890
891 url = "https://www.dy.fi/nic/update"
892
893 # Please only send automatic updates when your IP address changes,
894 # or once per 5 to 6 days to refresh the address mapping (they will
895 # expire if not refreshed within 7 days).
896 holdoff_days = 6
897
898
899 class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
900 handle = "dyndns.org"
901 name = "Dyn"
902 website = "http://dyn.com/dns/"
903 protocols = ("ipv4",)
904
905 # Information about the format of the request is to be found
906 # http://http://dyn.com/support/developers/api/perform-update/
907 # http://dyn.com/support/developers/api/return-codes/
908
909 url = "https://members.dyndns.org/nic/update"
910
911
912 class DDNSProviderDomainOffensive(DDNSProtocolDynDNS2, DDNSProvider):
913 handle = "do.de"
914 name = "Domain-Offensive"
915 website = "https://www.do.de/"
916 protocols = ("ipv6", "ipv4")
917
918 # Detailed information about the request and response codes
919 # are available on the providers webpage.
920 # https://www.do.de/wiki/FlexDNS_-_Entwickler
921
922 url = "https://ddns.do.de/"
923
924 class DDNSProviderDynUp(DDNSProvider):
925 handle = "dynup.de"
926 name = "DynUp.DE"
927 website = "http://dynup.de/"
928 protocols = ("ipv4",)
929
930 # Information about the format of the HTTPS request is to be found
931 # https://dyndnsfree.de/user/hilfe.php
932
933 url = "https://dynup.de/dyn.php"
934 can_remove_records = False
935 supports_token_auth = False
936
937 def update_protocol(self, proto):
938 data = {
939 "username" : self.username,
940 "password" : self.password,
941 "hostname" : self.hostname,
942 "print" : '1',
943 }
944
945 # Send update to the server.
946 response = self.send_request(self.url, data=data)
947
948 # Get the full response message.
949 output = response.read().decode()
950
951 # Remove all leading and trailing whitespace.
952 output = output.strip()
953
954 # Handle success messages.
955 if output.startswith("I:OK"):
956 return
957
958 # If we got here, some other update error happened.
959 raise DDNSUpdateError
960
961
962 class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
963 handle = "dynu.com"
964 name = "Dynu"
965 website = "http://dynu.com/"
966 protocols = ("ipv6", "ipv4",)
967
968 # Detailed information about the request and response codes
969 # are available on the providers webpage.
970 # http://dynu.com/Default.aspx?page=dnsapi
971
972 url = "https://api.dynu.com/nic/update"
973
974 # DynU sends the IPv6 and IPv4 address in one request
975
976 def update(self):
977 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
978
979 # This one supports IPv6
980 myipv6 = self.get_address("ipv6")
981
982 # Add update information if we have an IPv6 address.
983 if myipv6:
984 data["myipv6"] = myipv6
985
986 self.send_request(data)
987
988
989 class DDNSProviderEasyDNS(DDNSProvider):
990 handle = "easydns.com"
991 name = "EasyDNS"
992 website = "http://www.easydns.com/"
993 protocols = ("ipv4",)
994
995 # Detailed information about the request and response codes
996 # (API 1.3) are available on the providers webpage.
997 # https://fusion.easydns.com/index.php?/Knowledgebase/Article/View/102/7/dynamic-dns
998
999 url = "https://api.cp.easydns.com/dyn/tomato.php"
1000
1001 supports_token_auth = False
1002
1003 def update_protocol(self, proto):
1004 data = {
1005 "myip" : self.get_address(proto, "-"),
1006 "hostname" : self.hostname,
1007 }
1008
1009 # Send update to the server.
1010 response = self.send_request(self.url, data=data, username=self.username, password=self.password)
1011
1012 # Get the full response message.
1013 output = response.read().decode()
1014
1015 # Remove all leading and trailing whitespace.
1016 output = output.strip()
1017
1018 # Handle success messages.
1019 if output.startswith("NOERROR"):
1020 return
1021
1022 # Handle error codes.
1023 if output.startswith("NOACCESS"):
1024 raise DDNSAuthenticationError
1025
1026 elif output.startswith("NOSERVICE"):
1027 raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain"))
1028
1029 elif output.startswith("ILLEGAL INPUT"):
1030 raise DDNSRequestError(_("Invalid data has been sent"))
1031
1032 elif output.startswith("TOOSOON"):
1033 raise DDNSRequestError(_("Too frequent update requests have been sent"))
1034
1035 # If we got here, some other update error happened.
1036 raise DDNSUpdateError
1037
1038
1039 class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
1040 handle = "domopoli.de"
1041 name = "domopoli.de"
1042 website = "http://domopoli.de/"
1043 protocols = ("ipv4",)
1044
1045 # https://www.domopoli.de/?page=howto#DynDns_start
1046
1047 url = "http://dyndns.domopoli.de/nic/update"
1048
1049
1050 class DDNSProviderDynsNet(DDNSProvider):
1051 handle = "dyns.net"
1052 name = "DyNS"
1053 website = "http://www.dyns.net/"
1054 protocols = ("ipv4",)
1055 can_remove_records = False
1056 supports_token_auth = False
1057
1058 # There is very detailed informatio about how to send the update request and
1059 # the possible response codes. (Currently we are using the v1.1 proto)
1060 # http://www.dyns.net/documentation/technical/protocol/
1061
1062 url = "http://www.dyns.net/postscript011.php"
1063
1064 def update_protocol(self, proto):
1065 data = {
1066 "ip" : self.get_address(proto),
1067 "host" : self.hostname,
1068 "username" : self.username,
1069 "password" : self.password,
1070 }
1071
1072 # Send update to the server.
1073 response = self.send_request(self.url, data=data)
1074
1075 # Get the full response message.
1076 output = response.read().decode()
1077
1078 # Handle success messages.
1079 if output.startswith("200"):
1080 return
1081
1082 # Handle error codes.
1083 if output.startswith("400"):
1084 raise DDNSRequestError(_("Malformed request has been sent"))
1085 elif output.startswith("401"):
1086 raise DDNSAuthenticationError
1087 elif output.startswith("402"):
1088 raise DDNSRequestError(_("Too frequent update requests have been sent"))
1089 elif output.startswith("403"):
1090 raise DDNSInternalServerError
1091
1092 # If we got here, some other update error happened.
1093 raise DDNSUpdateError(_("Server response: %s") % output)
1094
1095
1096 class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
1097 handle = "enom.com"
1098 name = "eNom Inc."
1099 website = "http://www.enom.com/"
1100 protocols = ("ipv4",)
1101
1102 # There are very detailed information about how to send an update request and
1103 # the respone codes.
1104 # http://www.enom.com/APICommandCatalog/
1105
1106 url = "https://dynamic.name-services.com/interface.asp"
1107 can_remove_records = False
1108 supports_token_auth = False
1109
1110 def update_protocol(self, proto):
1111 data = {
1112 "command" : "setdnshost",
1113 "responsetype" : "xml",
1114 "address" : self.get_address(proto),
1115 "domainpassword" : self.password,
1116 "zone" : self.hostname
1117 }
1118
1119 # Send update to the server.
1120 response = self.send_request(self.url, data=data)
1121
1122 # Get the full response message.
1123 output = response.read().decode()
1124
1125 # Handle success messages.
1126 if self.get_xml_tag_value(output, "ErrCount") == "0":
1127 return
1128
1129 # Handle error codes.
1130 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
1131
1132 if errorcode == "304155":
1133 raise DDNSAuthenticationError
1134 elif errorcode == "304153":
1135 raise DDNSRequestError(_("Domain not found"))
1136
1137 # If we got here, some other update error happened.
1138 raise DDNSUpdateError
1139
1140
1141 class DDNSProviderEntryDNS(DDNSProvider):
1142 handle = "entrydns.net"
1143 name = "EntryDNS"
1144 website = "http://entrydns.net/"
1145 protocols = ("ipv4",)
1146
1147 # Some very tiny details about their so called "Simple API" can be found
1148 # here: https://entrydns.net/help
1149 url = "https://entrydns.net/records/modify"
1150 can_remove_records = False
1151 supports_token_auth = True
1152
1153 def update_protocol(self, proto):
1154 data = {
1155 "ip" : self.get_address(proto),
1156 }
1157
1158 # Add auth token to the update url.
1159 url = "%s/%s" % (self.url, self.token)
1160
1161 # Send update to the server.
1162 try:
1163 response = self.send_request(url, data=data)
1164
1165 # Handle error codes
1166 except urllib.error.HTTPError as e:
1167 if e.code == 404:
1168 raise DDNSAuthenticationError
1169
1170 elif e.code == 422:
1171 raise DDNSRequestError(_("An invalid IP address was submitted"))
1172
1173 raise
1174
1175 # Handle success messages.
1176 if response.code == 200:
1177 return
1178
1179 # If we got here, some other update error happened.
1180 raise DDNSUpdateError
1181
1182
1183 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
1184 handle = "freedns.afraid.org"
1185 name = "freedns.afraid.org"
1186 website = "http://freedns.afraid.org/"
1187
1188 # No information about the request or response could be found on the vendor
1189 # page. All used values have been collected by testing.
1190 url = "https://freedns.afraid.org/dynamic/update.php"
1191 can_remove_records = False
1192 supports_token_auth = True
1193
1194 def update_protocol(self, proto):
1195 data = {
1196 "address" : self.get_address(proto),
1197 }
1198
1199 # Add auth token to the update url.
1200 url = "%s?%s" % (self.url, self.token)
1201
1202 # Send update to the server.
1203 response = self.send_request(url, data=data)
1204
1205 # Get the full response message.
1206 output = response.read().decode()
1207
1208 # Handle success messages.
1209 if output.startswith("Updated") or "has not changed" in output:
1210 return
1211
1212 # Handle error codes.
1213 if output == "ERROR: Unable to locate this record":
1214 raise DDNSAuthenticationError
1215 elif "is an invalid IP address" in output:
1216 raise DDNSRequestError(_("Invalid IP address has been sent"))
1217
1218 # If we got here, some other update error happened.
1219 raise DDNSUpdateError
1220
1221
1222 class DDNSProviderHENet(DDNSProtocolDynDNS2, DDNSProvider):
1223 handle = "he.net"
1224 name = "he.net"
1225 website = "https://he.net"
1226 protocols = ("ipv6", "ipv4",)
1227
1228 # Detailed information about the update api can be found here.
1229 # http://dns.he.net/docs.html
1230
1231 url = "https://dyn.dns.he.net/nic/update"
1232 @property
1233 def username(self):
1234 return self.get("hostname")
1235
1236
1237
1238 class DDNSProviderItsdns(DDNSProtocolDynDNS2, DDNSProvider):
1239 handle = "inwx.com"
1240 name = "INWX"
1241 website = "https://www.inwx.com"
1242 protocols = ("ipv6", "ipv4")
1243
1244 # Information about the format of the HTTP request is to be found
1245 # here: https://www.inwx.com/en/nameserver2/dyndns (requires login)
1246 # Notice: The URL is the same for: inwx.com|de|at|ch|es
1247
1248 url = "https://dyndns.inwx.com/nic/update"
1249
1250
1251 class DDNSProviderItsdns(DDNSProtocolDynDNS2, DDNSProvider):
1252 handle = "itsdns.de"
1253 name = "it's DNS"
1254 website = "http://www.itsdns.de/"
1255 protocols = ("ipv6", "ipv4")
1256
1257 # Information about the format of the HTTP request is to be found
1258 # here: https://www.itsdns.de/dynupdatehelp.htm
1259
1260 url = "https://www.itsdns.de/update.php"
1261
1262
1263 class DDNSProviderJoker(DDNSProtocolDynDNS2, DDNSProvider):
1264 handle = "joker.com"
1265 name = "Joker.com Dynamic DNS"
1266 website = "https://joker.com/"
1267 protocols = ("ipv4",)
1268
1269 # Information about the request can be found here:
1270 # https://joker.com/faq/content/11/427/en/what-is-dynamic-dns-dyndns.html
1271 # Using DynDNS V2 protocol over HTTPS here
1272
1273 url = "https://svc.joker.com/nic/update"
1274
1275
1276 class DDNSProviderKEYSYSTEMS(DDNSProvider):
1277 handle = "key-systems.net"
1278 name = "dynamicdns.key-systems.net"
1279 website = "https://domaindiscount24.com/"
1280 protocols = ("ipv4",)
1281
1282 # There are only information provided by the domaindiscount24 how to
1283 # perform an update with HTTP APIs
1284 # https://www.domaindiscount24.com/faq/dynamic-dns
1285 # examples: https://dynamicdns.key-systems.net/update.php?hostname=hostname&password=password&ip=auto
1286 # https://dynamicdns.key-systems.net/update.php?hostname=hostname&password=password&ip=213.x.x.x&mx=213.x.x.x
1287
1288 url = "https://dynamicdns.key-systems.net/update.php"
1289 can_remove_records = False
1290 supports_token_auth = False
1291
1292 def update_protocol(self, proto):
1293 address = self.get_address(proto)
1294 data = {
1295 "hostname" : self.hostname,
1296 "password" : self.password,
1297 "ip" : address,
1298 }
1299
1300 # Send update to the server.
1301 response = self.send_request(self.url, data=data)
1302
1303 # Get the full response message.
1304 output = response.read().decode()
1305
1306 # Handle success messages.
1307 if "code = 200" in output:
1308 return
1309
1310 # Handle error messages.
1311 if "abuse prevention triggered" in output:
1312 raise DDNSAbuseError
1313 elif "invalid password" in output:
1314 raise DDNSAuthenticationError
1315 elif "Authorization failed" in output:
1316 raise DDNSRequestError(_("Invalid hostname specified"))
1317
1318 # If we got here, some other update error happened.
1319 raise DDNSUpdateError
1320
1321
1322 class DDNSProviderGoogle(DDNSProtocolDynDNS2, DDNSProvider):
1323 handle = "domains.google.com"
1324 name = "Google Domains"
1325 website = "https://domains.google.com/"
1326 protocols = ("ipv4",)
1327
1328 # Information about the format of the HTTP request is to be found
1329 # here: https://support.google.com/domains/answer/6147083?hl=en
1330
1331 url = "https://domains.google.com/nic/update"
1332
1333
1334 class DDNSProviderLightningWireLabs(DDNSProvider):
1335 handle = "dns.lightningwirelabs.com"
1336 name = "Lightning Wire Labs DNS Service"
1337 website = "https://dns.lightningwirelabs.com/"
1338
1339 # Information about the format of the HTTPS request is to be found
1340 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
1341
1342 supports_token_auth = True
1343
1344 url = "https://dns.lightningwirelabs.com/update"
1345
1346 def update(self):
1347 # Raise an error if no auth details are given.
1348 if not self.token:
1349 raise DDNSConfigurationError
1350
1351 data = {
1352 "hostname" : self.hostname,
1353 "token" : self.token,
1354 "address6" : self.get_address("ipv6", "-"),
1355 "address4" : self.get_address("ipv4", "-"),
1356 }
1357
1358 # Send update to the server.
1359 response = self.send_request(self.url, data=data)
1360
1361 # Handle success messages.
1362 if response.code == 200:
1363 return
1364
1365 # If we got here, some other update error happened.
1366 raise DDNSUpdateError
1367
1368
1369 class DDNSProviderLoopia(DDNSProtocolDynDNS2, DDNSProvider):
1370 handle = "loopia.se"
1371 name = "Loopia AB"
1372 website = "https://www.loopia.com"
1373 protocols = ("ipv4",)
1374
1375 # Information about the format of the HTTP request is to be found
1376 # here: https://support.loopia.com/wiki/About_the_DynDNS_support
1377
1378 url = "https://dns.loopia.se/XDynDNSServer/XDynDNS.php"
1379
1380
1381 class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
1382 handle = "myonlineportal.net"
1383 name = "myonlineportal.net"
1384 website = "https:/myonlineportal.net/"
1385
1386 # Information about the request and response can be obtained here:
1387 # https://myonlineportal.net/howto_dyndns
1388
1389 url = "https://myonlineportal.net/updateddns"
1390
1391 def prepare_request_data(self, proto):
1392 data = {
1393 "hostname" : self.hostname,
1394 "ip" : self.get_address(proto),
1395 }
1396
1397 return data
1398
1399
1400 class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
1401 handle = "namecheap.com"
1402 name = "Namecheap"
1403 website = "http://namecheap.com"
1404 protocols = ("ipv4",)
1405
1406 # Information about the format of the HTTP request is to be found
1407 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
1408 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
1409
1410 url = "https://dynamicdns.park-your-domain.com/update"
1411 can_remove_records = False
1412 supports_token_auth = False
1413
1414 def update_protocol(self, proto):
1415 # Namecheap requires the hostname splitted into a host and domain part.
1416 host, domain = self.hostname.split(".", 1)
1417
1418 # Get and store curent IP address.
1419 address = self.get_address(proto)
1420
1421 data = {
1422 "ip" : address,
1423 "password" : self.password,
1424 "host" : host,
1425 "domain" : domain
1426 }
1427
1428 # Send update to the server.
1429 response = self.send_request(self.url, data=data)
1430
1431 # Get the full response message.
1432 output = response.read().decode()
1433
1434 # Handle success messages.
1435 if self.get_xml_tag_value(output, "IP") == address:
1436 return
1437
1438 # Handle error codes.
1439 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
1440
1441 if errorcode == "304156":
1442 raise DDNSAuthenticationError
1443 elif errorcode == "316153":
1444 raise DDNSRequestError(_("Domain not found"))
1445 elif errorcode == "316154":
1446 raise DDNSRequestError(_("Domain not active"))
1447 elif errorcode in ("380098", "380099"):
1448 raise DDNSInternalServerError
1449
1450 # If we got here, some other update error happened.
1451 raise DDNSUpdateError
1452
1453
1454 class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
1455 handle = "no-ip.com"
1456 name = "NoIP"
1457 website = "http://www.noip.com/"
1458 protocols = ("ipv4",)
1459
1460 # Information about the format of the HTTP request is to be found
1461 # here: http://www.noip.com/integrate/request and
1462 # here: http://www.noip.com/integrate/response
1463
1464 url = "https://dynupdate.noip.com/nic/update"
1465
1466 def prepare_request_data(self, proto):
1467 assert proto == "ipv4"
1468
1469 data = {
1470 "hostname" : self.hostname,
1471 "address" : self.get_address(proto),
1472 }
1473
1474 return data
1475
1476
1477 class DDNSProviderNowDNS(DDNSProtocolDynDNS2, DDNSProvider):
1478 handle = "now-dns.com"
1479 name = "NOW-DNS"
1480 website = "http://now-dns.com/"
1481 protocols = ("ipv6", "ipv4")
1482
1483 # Information about the format of the request is to be found
1484 # but only can be accessed by register an account and login
1485 # https://now-dns.com/?m=api
1486
1487 url = "https://now-dns.com/update"
1488
1489
1490 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
1491 handle = "nsupdate.info"
1492 name = "nsupdate.info"
1493 website = "http://nsupdate.info/"
1494 protocols = ("ipv6", "ipv4",)
1495
1496 # Information about the format of the HTTP request can be found
1497 # after login on the provider user interface and here:
1498 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1499
1500 url = "https://nsupdate.info/nic/update"
1501
1502 # TODO nsupdate.info can actually do this, but the functionality
1503 # has not been implemented here, yet.
1504 can_remove_records = False
1505
1506 supports_token_auth = True
1507
1508 # After a failed update, there will be no retries
1509 # https://bugzilla.ipfire.org/show_bug.cgi?id=10603
1510 holdoff_failure_days = None
1511
1512 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1513 # and for the password a so called secret.
1514 @property
1515 def username(self):
1516 return self.get("hostname")
1517
1518 @property
1519 def password(self):
1520 return self.token or self.get("secret")
1521
1522 def prepare_request_data(self, proto):
1523 data = {
1524 "myip" : self.get_address(proto),
1525 }
1526
1527 return data
1528
1529
1530 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
1531 handle = "opendns.com"
1532 name = "OpenDNS"
1533 website = "http://www.opendns.com"
1534
1535 # Detailed information about the update request and possible
1536 # response codes can be obtained from here:
1537 # https://support.opendns.com/entries/23891440
1538
1539 url = "https://updates.opendns.com/nic/update"
1540
1541 def prepare_request_data(self, proto):
1542 data = {
1543 "hostname" : self.hostname,
1544 "myip" : self.get_address(proto),
1545 }
1546
1547 return data
1548
1549
1550 class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1551 handle = "ovh.com"
1552 name = "OVH"
1553 website = "http://www.ovh.com/"
1554 protocols = ("ipv4",)
1555
1556 # OVH only provides very limited information about how to
1557 # update a DynDNS host. They only provide the update url
1558 # on the their german subpage.
1559 #
1560 # http://hilfe.ovh.de/DomainDynHost
1561
1562 url = "https://www.ovh.com/nic/update"
1563
1564 def prepare_request_data(self, proto):
1565 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1566 data.update({
1567 "system" : "dyndns",
1568 })
1569
1570 return data
1571
1572
1573 class DDNSProviderRegfish(DDNSProvider):
1574 handle = "regfish.com"
1575 name = "Regfish GmbH"
1576 website = "http://www.regfish.com/"
1577
1578 # A full documentation to the providers api can be found here
1579 # but is only available in german.
1580 # https://www.regfish.de/domains/dyndns/dokumentation
1581
1582 url = "https://dyndns.regfish.de/"
1583 can_remove_records = False
1584 supports_token_auth = True
1585
1586 def update(self):
1587 data = {
1588 "fqdn" : self.hostname,
1589 }
1590
1591 # Check if we update an IPv6 address.
1592 address6 = self.get_address("ipv6")
1593 if address6:
1594 data["ipv6"] = address6
1595
1596 # Check if we update an IPv4 address.
1597 address4 = self.get_address("ipv4")
1598 if address4:
1599 data["ipv4"] = address4
1600
1601 # Raise an error if none address is given.
1602 if "ipv6" not in data and "ipv4" not in data:
1603 raise DDNSConfigurationError
1604
1605 # Check if a token has been set.
1606 if self.token:
1607 data["token"] = self.token
1608
1609 # Raise an error if no token and no useranem and password
1610 # are given.
1611 elif not self.username and not self.password:
1612 raise DDNSConfigurationError(_("No Auth details specified"))
1613
1614 # HTTP Basic Auth is only allowed if no token is used.
1615 if self.token:
1616 # Send update to the server.
1617 response = self.send_request(self.url, data=data)
1618 else:
1619 # Send update to the server.
1620 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
1621
1622 # Get the full response message.
1623 output = response.read().decode()
1624
1625 # Handle success messages.
1626 if "100" in output or "101" in output:
1627 return
1628
1629 # Handle error codes.
1630 if "401" or "402" in output:
1631 raise DDNSAuthenticationError
1632 elif "408" in output:
1633 raise DDNSRequestError(_("Invalid IPv4 address has been sent"))
1634 elif "409" in output:
1635 raise DDNSRequestError(_("Invalid IPv6 address has been sent"))
1636 elif "412" in output:
1637 raise DDNSRequestError(_("No valid FQDN was given"))
1638 elif "414" in output:
1639 raise DDNSInternalServerError
1640
1641 # If we got here, some other update error happened.
1642 raise DDNSUpdateError
1643
1644
1645 class DDNSProviderSchokokeksDNS(DDNSProtocolDynDNS2, DDNSProvider):
1646 handle = "schokokeks.org"
1647 name = "Schokokeks"
1648 website = "http://www.schokokeks.org/"
1649 protocols = ("ipv4",)
1650
1651 # Information about the format of the request is to be found
1652 # https://wiki.schokokeks.org/DynDNS
1653 url = "https://dyndns.schokokeks.org/nic/update"
1654
1655
1656 class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
1657 handle = "selfhost.de"
1658 name = "Selfhost.de"
1659 website = "http://www.selfhost.de/"
1660 protocols = ("ipv4",)
1661
1662 url = "https://carol.selfhost.de/nic/update"
1663
1664 def prepare_request_data(self, proto):
1665 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1666 data.update({
1667 "hostname" : "1",
1668 })
1669
1670 return data
1671
1672
1673 class DDNSProviderServercow(DDNSProvider):
1674 handle = "servercow.de"
1675 name = "servercow.de"
1676 website = "https://servercow.de/"
1677 protocols = ("ipv4", "ipv6")
1678
1679 url = "https://www.servercow.de/dnsupdate/update.php"
1680 can_remove_records = False
1681 supports_token_auth = False
1682
1683 def update_protocol(self, proto):
1684 data = {
1685 "ipaddr" : self.get_address(proto),
1686 "hostname" : self.hostname,
1687 "username" : self.username,
1688 "pass" : self.password,
1689 }
1690
1691 # Send request to provider
1692 response = self.send_request(self.url, data=data)
1693
1694 # Read response
1695 output = response.read().decode()
1696
1697 # Server responds with OK if update was successful
1698 if output.startswith("OK"):
1699 return
1700
1701 # Catch any errors
1702 elif output.startswith("FAILED - Authentication failed"):
1703 raise DDNSAuthenticationError
1704
1705 # If we got here, some other update error happened
1706 raise DDNSUpdateError(output)
1707
1708
1709 class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1710 handle = "spdns.org"
1711 name = "SPDYN"
1712 website = "https://www.spdyn.de/"
1713
1714 # Detailed information about request and response codes are provided
1715 # by the vendor. They are using almost the same mechanism and status
1716 # codes as dyndns.org so we can inherit all those stuff.
1717 #
1718 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1719 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1720
1721 url = "https://update.spdyn.de/nic/update"
1722
1723 supports_token_auth = True
1724
1725 @property
1726 def username(self):
1727 return self.get("username") or self.hostname
1728
1729 @property
1730 def password(self):
1731 return self.get("password") or self.token
1732
1733
1734 class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1735 handle = "strato.com"
1736 name = "Strato AG"
1737 website = "http:/www.strato.com/"
1738 protocols = ("ipv4",)
1739
1740 # Information about the request and response can be obtained here:
1741 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1742
1743 url = "https://dyndns.strato.com/nic/update"
1744
1745 def prepare_request_data(self, proto):
1746 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1747 data.update({
1748 "mx" : "NOCHG",
1749 "backupmx" : "NOCHG"
1750 })
1751
1752 return data
1753
1754
1755 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1756 handle = "twodns.de"
1757 name = "TwoDNS"
1758 website = "http://www.twodns.de"
1759 protocols = ("ipv4",)
1760
1761 # Detailed information about the request can be found here
1762 # http://twodns.de/en/faqs
1763 # http://twodns.de/en/api
1764
1765 url = "https://update.twodns.de/update"
1766
1767 def prepare_request_data(self, proto):
1768 assert proto == "ipv4"
1769
1770 data = {
1771 "ip" : self.get_address(proto),
1772 "hostname" : self.hostname
1773 }
1774
1775 return data
1776
1777
1778 class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1779 handle = "udmedia.de"
1780 name = "Udmedia GmbH"
1781 website = "http://www.udmedia.de"
1782 protocols = ("ipv4",)
1783
1784 # Information about the request can be found here
1785 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1786
1787 url = "https://www.udmedia.de/nic/update"
1788
1789
1790 class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
1791 handle = "variomedia.de"
1792 name = "Variomedia"
1793 website = "http://www.variomedia.de/"
1794 protocols = ("ipv6", "ipv4",)
1795
1796 # Detailed information about the request can be found here
1797 # https://dyndns.variomedia.de/
1798
1799 url = "https://dyndns.variomedia.de/nic/update"
1800
1801 def prepare_request_data(self, proto):
1802 data = {
1803 "hostname" : self.hostname,
1804 "myip" : self.get_address(proto),
1805 }
1806
1807 return data
1808
1809
1810 class DDNSProviderXLhost(DDNSProtocolDynDNS2, DDNSProvider):
1811 handle = "xlhost.de"
1812 name = "XLhost"
1813 website = "http://xlhost.de/"
1814 protocols = ("ipv4",)
1815
1816 # Information about the format of the HTTP request is to be found
1817 # here: https://xlhost.de/faq/index_html?topicId=CQA2ELIPO4SQ
1818
1819 url = "https://nsupdate.xlhost.de/"
1820
1821
1822 class DDNSProviderZoneedit(DDNSProvider):
1823 handle = "zoneedit.com"
1824 name = "Zoneedit"
1825 website = "http://www.zoneedit.com"
1826 protocols = ("ipv4",)
1827
1828 supports_token_auth = False
1829
1830 # Detailed information about the request and the response codes can be
1831 # obtained here:
1832 # http://www.zoneedit.com/doc/api/other.html
1833 # http://www.zoneedit.com/faq.html
1834
1835 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1836
1837 def update_protocol(self, proto):
1838 data = {
1839 "dnsto" : self.get_address(proto),
1840 "host" : self.hostname
1841 }
1842
1843 # Send update to the server.
1844 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
1845
1846 # Get the full response message.
1847 output = response.read().decode()
1848
1849 # Handle success messages.
1850 if output.startswith("<SUCCESS"):
1851 return
1852
1853 # Handle error codes.
1854 if output.startswith("invalid login"):
1855 raise DDNSAuthenticationError
1856 elif output.startswith("<ERROR CODE=\"704\""):
1857 raise DDNSRequestError(_("No valid FQDN was given"))
1858 elif output.startswith("<ERROR CODE=\"702\""):
1859 raise DDNSRequestError(_("Too frequent update requests have been sent"))
1860
1861 # If we got here, some other update error happened.
1862 raise DDNSUpdateError
1863
1864
1865 class DDNSProviderDNSmadeEasy(DDNSProvider):
1866 handle = "dnsmadeeasy.com"
1867 name = "DNSmadeEasy.com"
1868 website = "http://www.dnsmadeeasy.com/"
1869 protocols = ("ipv4",)
1870
1871 # DNS Made Easy Nameserver Provider also offering Dynamic DNS
1872 # Documentation can be found here:
1873 # http://www.dnsmadeeasy.com/dynamic-dns/
1874
1875 url = "https://cp.dnsmadeeasy.com/servlet/updateip?"
1876 can_remove_records = False
1877 supports_token_auth = False
1878
1879 def update_protocol(self, proto):
1880 data = {
1881 "ip" : self.get_address(proto),
1882 "id" : self.hostname,
1883 "username" : self.username,
1884 "password" : self.password,
1885 }
1886
1887 # Send update to the server.
1888 response = self.send_request(self.url, data=data)
1889
1890 # Get the full response message.
1891 output = response.read().decode()
1892
1893 # Handle success messages.
1894 if output.startswith("success") or output.startswith("error-record-ip-same"):
1895 return
1896
1897 # Handle error codes.
1898 if output.startswith("error-auth-suspend"):
1899 raise DDNSRequestError(_("Account has been suspended"))
1900
1901 elif output.startswith("error-auth-voided"):
1902 raise DDNSRequestError(_("Account has been revoked"))
1903
1904 elif output.startswith("error-record-invalid"):
1905 raise DDNSRequestError(_("Specified host does not exist"))
1906
1907 elif output.startswith("error-auth"):
1908 raise DDNSAuthenticationError
1909
1910 # If we got here, some other update error happened.
1911 raise DDNSUpdateError(_("Server response: %s") % output)
1912
1913
1914 class DDNSProviderZZZZ(DDNSProvider):
1915 handle = "zzzz.io"
1916 name = "zzzz"
1917 website = "https://zzzz.io"
1918 protocols = ("ipv6", "ipv4",)
1919
1920 # Detailed information about the update request can be found here:
1921 # https://zzzz.io/faq/
1922
1923 # Details about the possible response codes have been provided in the bugtracker:
1924 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1925
1926 url = "https://zzzz.io/api/v1/update"
1927 can_remove_records = False
1928 supports_token_auth = True
1929
1930 def update_protocol(self, proto):
1931 data = {
1932 "ip" : self.get_address(proto),
1933 "token" : self.token,
1934 }
1935
1936 if proto == "ipv6":
1937 data["type"] = "aaaa"
1938
1939 # zzzz uses the host from the full hostname as part
1940 # of the update url.
1941 host, domain = self.hostname.split(".", 1)
1942
1943 # Add host value to the update url.
1944 url = "%s/%s" % (self.url, host)
1945
1946 # Send update to the server.
1947 try:
1948 response = self.send_request(url, data=data)
1949
1950 # Handle error codes.
1951 except DDNSNotFound:
1952 raise DDNSRequestError(_("Invalid hostname specified"))
1953
1954 # Handle success messages.
1955 if response.code == 200:
1956 return
1957
1958 # If we got here, some other update error happened.
1959 raise DDNSUpdateError