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