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