]> git.ipfire.org Git - oddments/ddns.git/blame - src/ddns/providers.py
DuckDNS: Update to use new API.
[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
64d3fad4 24import os
a892c594 25import subprocess
91aead36
KB
26import urllib.request
27import urllib.error
28import urllib.parse
d1cd57eb 29import xml.dom.minidom
7399fc5b 30
91aead36 31from .i18n import _
7399fc5b 32
f22ab085
MT
33# Import all possible exception types.
34from .errors import *
35
7399fc5b
MT
36logger = logging.getLogger("ddns.providers")
37logger.propagate = 1
38
adfe6272
MT
39_providers = {}
40
41def get():
42 """
43 Returns a dict with all automatically registered providers.
44 """
45 return _providers.copy()
46
f22ab085 47class DDNSProvider(object):
6a11646e
MT
48 # A short string that uniquely identifies
49 # this provider.
50 handle = None
f22ab085 51
6a11646e
MT
52 # The full name of the provider.
53 name = None
f22ab085 54
6a11646e
MT
55 # A weburl to the homepage of the provider.
56 # (Where to register a new account?)
57 website = None
f22ab085 58
6a11646e
MT
59 # A list of supported protocols.
60 protocols = ("ipv6", "ipv4")
f22ab085
MT
61
62 DEFAULT_SETTINGS = {}
63
37e24fbf
MT
64 # holdoff time - Number of days no update is performed unless
65 # the IP address has changed.
66 holdoff_days = 30
67
112d3fb8
MT
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
29c8c9c6
MT
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
64d3fad4
MT
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
f22ab085
MT
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
571271bc
MT
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
f22ab085
MT
104 def __repr__(self):
105 return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
106
107 def __cmp__(self, other):
91aead36 108 return (lambda a, b: (a > b)-(a < b))(self.hostname, other.hostname)
f22ab085 109
37e24fbf
MT
110 @property
111 def db(self):
112 return self.core.db
113
f22ab085
MT
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
46687828
SS
141 @property
142 def token(self):
143 """
144 Fast access to the token.
145 """
146 return self.get("token")
147
9da3e685
MT
148 def __call__(self, force=False):
149 if force:
c3888f15 150 logger.debug(_("Updating %s forced") % self.hostname)
9da3e685 151
112d3fb8
MT
152 # Do nothing if the last update has failed or no update is required
153 elif self.has_failure or not self.requires_update:
7399fc5b
MT
154 return
155
156 # Execute the update.
37e24fbf
MT
157 try:
158 self.update()
159
2e09c70d 160 # 1) Catch network errors early, because we do not want to log
29a69850
MT
161 # them to the database. They are usually temporary and caused
162 # by the client side, so that we will retry quickly.
2e09c70d
MT
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):
29a69850
MT
167 raise
168
37e24fbf
MT
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
5f402f36 174
91aead36
KB
175 logger.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") %
176 {"hostname": self.hostname, "provider": self.name})
37e24fbf 177 self.core.db.log_success(self.hostname)
12b3818b 178
5f402f36 179 def update(self):
d45139f6
MT
180 for protocol in self.protocols:
181 if self.have_address(protocol):
182 self.update_protocol(protocol)
29c8c9c6 183 elif self.can_remove_records:
d45139f6
MT
184 self.remove_protocol(protocol)
185
186 def update_protocol(self, proto):
f22ab085
MT
187 raise NotImplementedError
188
d45139f6 189 def remove_protocol(self, proto):
29c8c9c6 190 if not self.can_remove_records:
91aead36 191 raise RuntimeError("can_remove_records is enabled, but remove_protocol() not implemented")
d45139f6 192
29c8c9c6 193 raise NotImplementedError
d45139f6 194
37e24fbf
MT
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):
91aead36
KB
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})
37e24fbf
MT
201
202 return True
203
204 # If the holdoff time has expired, an update is required, too
205 if self.holdoff_time_expired():
91aead36
KB
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})
37e24fbf
MT
208
209 return True
210
211 # Otherwise, we don't need to perform an update
91aead36
KB
212 logger.debug(_("No update required for %(hostname)s (%(provider)s)") %
213 {"hostname": self.hostname, "provider": self.name})
37e24fbf
MT
214
215 return False
216
112d3fb8
MT
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
64018439
MT
229 # If there is no holdoff time, we won't update ever again.
230 if self.holdoff_failure_days is None:
91aead36 231 logger.warning(_("An update has not been performed because earlier updates failed for %s") % self.hostname)
64018439
MT
232 logger.warning(_("There will be no retries"))
233
234 return True
235
112d3fb8
MT
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
91aead36 244 logger.warning(_("An update has not been performed because earlier updates failed for %s") % self.hostname)
112d3fb8
MT
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
37e24fbf 258 def ip_address_changed(self, protos):
7399fc5b
MT
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)
7399fc5b
MT
266 current_address = self.get_address(proto)
267
29c8c9c6
MT
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...
38d81db4
MT
277 continue
278
7399fc5b 279 if not current_address in addresses:
37e24fbf
MT
280 return True
281
282 return False
7399fc5b 283
37e24fbf
MT
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
112d3fb8 294 last_update = self.db.last_update(self.hostname, status="success")
37e24fbf
MT
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
7399fc5b 300
37e24fbf
MT
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:
91aead36
KB
310 logger.debug("Updates for %s are held off until %s" %
311 (self.hostname, holdoff_end))
37e24fbf 312 return False
7399fc5b 313
f22ab085
MT
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
e3c70807 321 def get_address(self, proto, default=None):
f22ab085
MT
322 """
323 Proxy method to get the current IP address.
324 """
e3c70807 325 return self.core.system.get_address(proto) or default
f22ab085 326
d45139f6
MT
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
f22ab085 339
5d4bec40
MT
340class 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
486c1b9d 349 # http://dyn.com/support/developers/api/perform-update/
5d4bec40
MT
350 # http://dyn.com/support/developers/api/return-codes/
351
29c8c9c6
MT
352 # The DynDNS protocol version 2 does not allow to remove records
353 can_remove_records = False
354
d45139f6 355 def prepare_request_data(self, proto):
5d4bec40
MT
356 data = {
357 "hostname" : self.hostname,
d45139f6 358 "myip" : self.get_address(proto),
5d4bec40
MT
359 }
360
361 return data
362
d45139f6
MT
363 def update_protocol(self, proto):
364 data = self.prepare_request_data(proto)
365
366 return self.send_request(data)
5d4bec40 367
d45139f6 368 def send_request(self, data):
5d4bec40 369 # Send update to the server.
91aead36 370 response = DDNSProvider.send_request(self, self.url, data=data, username=self.username, password=self.password)
5d4bec40
MT
371
372 # Get the full response message.
472b2408 373 output = response.read().decode()
5d4bec40
MT
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
af97e369 382 elif output == "abuse":
5d4bec40
MT
383 raise DDNSAbuseError
384 elif output == "notfqdn":
fb701db9 385 raise DDNSRequestError(_("No valid FQDN was given"))
5d4bec40 386 elif output == "nohost":
fb701db9 387 raise DDNSRequestError(_("Specified host does not exist"))
5d4bec40
MT
388 elif output == "911":
389 raise DDNSInternalServerError
390 elif output == "dnserr":
fb701db9 391 raise DDNSInternalServerError(_("DNS error encountered"))
6ddfd5c7
SS
392 elif output == "badagent":
393 raise DDNSBlockedError
9db9ea25
MP
394 elif output == "badip":
395 raise DDNSBlockedError
5d4bec40
MT
396
397 # If we got here, some other update error happened.
398 raise DDNSUpdateError(_("Server response: %s") % output)
399
400
78c9780b
SS
401class 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.
91aead36 407 """
78c9780b
SS
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
3b16fdb1 430class DDNSProviderAllInkl(DDNSProvider):
6a11646e
MT
431 handle = "all-inkl.com"
432 name = "All-inkl.com"
433 website = "http://all-inkl.com/"
434 protocols = ("ipv4",)
3b16fdb1
SS
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"
29c8c9c6 442 can_remove_records = False
3b16fdb1
SS
443
444 def update(self):
3b16fdb1
SS
445 # There is no additional data required so we directly can
446 # send our request.
536e87d1 447 response = self.send_request(self.url, username=self.username, password=self.password)
3b16fdb1
SS
448
449 # Get the full response message.
472b2408 450 output = response.read().decode()
3b16fdb1
SS
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
a892c594
MT
460class DDNSProviderBindNsupdate(DDNSProvider):
461 handle = "nsupdate"
462 name = "BIND nsupdate utility"
463 website = "http://en.wikipedia.org/wiki/Nsupdate"
464
465 DEFAULT_TTL = 60
466
64d3fad4
MT
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
a892c594
MT
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
91aead36 488 p = subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
a892c594
MT
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
97998aac
MW
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
a892c594
MT
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
78046ffe
SS
542class 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:
91aead36 562 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
78046ffe
SS
563
564 # Handle error codes.
91aead36 565 except urllib.error.HTTPError as e:
78046ffe
SS
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
d1a5be9e
JD
579class 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
f1332a16
SS
606class 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.
f1332a16 645 # Get status information from the header.
7d0956d1 646 output = response.getheader('ddnss-response')
f1332a16
SS
647
648 # Handle success messages.
649 if output == "good" or output == "nochg":
650 return
651
652 # Handle error codes.
653 if output == "badauth":
654 raise DDNSAuthenticationError
655 elif output == "notfqdn":
fb701db9 656 raise DDNSRequestError(_("No valid FQDN was given"))
f1332a16 657 elif output == "nohost":
fb701db9 658 raise DDNSRequestError(_("Specified host does not exist"))
f1332a16
SS
659 elif output == "911":
660 raise DDNSInternalServerError
661 elif output == "dnserr":
fb701db9 662 raise DDNSInternalServerError(_("DNS error encountered"))
f1332a16 663 elif output == "disabled":
fb701db9 664 raise DDNSRequestError(_("Account disabled or locked"))
f1332a16
SS
665
666 # If we got here, some other update error happened.
667 raise DDNSUpdateError
668
669
f3cf1f70 670class DDNSProviderDHS(DDNSProvider):
6a11646e
MT
671 handle = "dhs.org"
672 name = "DHS International"
673 website = "http://dhs.org/"
674 protocols = ("ipv4",)
f3cf1f70
SS
675
676 # No information about the used update api provided on webpage,
677 # grabed from source code of ez-ipudate.
b2b05ef3 678
f3cf1f70 679 url = "http://members.dhs.org/nic/hosts"
29c8c9c6 680 can_remove_records = False
f3cf1f70 681
d45139f6 682 def update_protocol(self, proto):
f3cf1f70
SS
683 data = {
684 "domain" : self.hostname,
d45139f6 685 "ip" : self.get_address(proto),
f3cf1f70
SS
686 "hostcmd" : "edit",
687 "hostcmdstage" : "2",
688 "type" : "4",
689 }
690
691 # Send update to the server.
91aead36 692 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
f3cf1f70
SS
693
694 # Handle success messages.
695 if response.code == 200:
696 return
697
f3cf1f70
SS
698 # If we got here, some other update error happened.
699 raise DDNSUpdateError
700
701
39301272 702class DDNSProviderDNSpark(DDNSProvider):
6a11646e
MT
703 handle = "dnspark.com"
704 name = "DNS Park"
705 website = "http://dnspark.com/"
706 protocols = ("ipv4",)
39301272
SS
707
708 # Informations to the used api can be found here:
709 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
b2b05ef3 710
39301272 711 url = "https://control.dnspark.com/api/dynamic/update.php"
29c8c9c6 712 can_remove_records = False
39301272 713
d45139f6 714 def update_protocol(self, proto):
39301272
SS
715 data = {
716 "domain" : self.hostname,
d45139f6 717 "ip" : self.get_address(proto),
39301272
SS
718 }
719
720 # Send update to the server.
91aead36 721 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
39301272
SS
722
723 # Get the full response message.
472b2408 724 output = response.read().decode()
39301272
SS
725
726 # Handle success messages.
727 if output.startswith("ok") or output.startswith("nochange"):
728 return
729
730 # Handle error codes.
731 if output == "unauth":
732 raise DDNSAuthenticationError
733 elif output == "abuse":
734 raise DDNSAbuseError
735 elif output == "blocked":
736 raise DDNSBlockedError
737 elif output == "nofqdn":
fb701db9 738 raise DDNSRequestError(_("No valid FQDN was given"))
39301272 739 elif output == "nohost":
fb701db9 740 raise DDNSRequestError(_("Invalid hostname specified"))
39301272 741 elif output == "notdyn":
fb701db9 742 raise DDNSRequestError(_("Hostname not marked as a dynamic host"))
39301272 743 elif output == "invalid":
fb701db9 744 raise DDNSRequestError(_("Invalid IP address has been sent"))
39301272
SS
745
746 # If we got here, some other update error happened.
747 raise DDNSUpdateError
748
43b2cd59
SS
749
750class DDNSProviderDtDNS(DDNSProvider):
6a11646e
MT
751 handle = "dtdns.com"
752 name = "DtDNS"
753 website = "http://dtdns.com/"
754 protocols = ("ipv4",)
43b2cd59
SS
755
756 # Information about the format of the HTTPS request is to be found
757 # http://www.dtdns.com/dtsite/updatespec
b2b05ef3 758
43b2cd59 759 url = "https://www.dtdns.com/api/autodns.cfm"
29c8c9c6 760 can_remove_records = False
43b2cd59 761
d45139f6 762 def update_protocol(self, proto):
43b2cd59 763 data = {
d45139f6 764 "ip" : self.get_address(proto),
43b2cd59
SS
765 "id" : self.hostname,
766 "pw" : self.password
767 }
768
769 # Send update to the server.
770 response = self.send_request(self.url, data=data)
771
772 # Get the full response message.
472b2408 773 output = response.read().decode()
43b2cd59
SS
774
775 # Remove all leading and trailing whitespace.
776 output = output.strip()
777
778 # Handle success messages.
779 if "now points to" in output:
780 return
781
782 # Handle error codes.
783 if output == "No hostname to update was supplied.":
fb701db9 784 raise DDNSRequestError(_("No hostname specified"))
43b2cd59
SS
785
786 elif output == "The hostname you supplied is not valid.":
fb701db9 787 raise DDNSRequestError(_("Invalid hostname specified"))
43b2cd59
SS
788
789 elif output == "The password you supplied is not valid.":
790 raise DDNSAuthenticationError
791
792 elif output == "Administration has disabled this account.":
fb701db9 793 raise DDNSRequestError(_("Account has been disabled"))
43b2cd59
SS
794
795 elif output == "Illegal character in IP.":
fb701db9 796 raise DDNSRequestError(_("Invalid IP address has been sent"))
43b2cd59
SS
797
798 elif output == "Too many failed requests.":
fb701db9 799 raise DDNSRequestError(_("Too many failed requests"))
43b2cd59
SS
800
801 # If we got here, some other update error happened.
802 raise DDNSUpdateError
803
804
ebdb3724 805class DDNSProviderDuckDNS(DDNSProvider):
fc91be92
MT
806 handle = "duckdns.org"
807 name = "Duck DNS"
808 website = "http://www.duckdns.org/"
ebdb3724 809 protocols = ("ipv6", "ipv4",)
fc91be92
MT
810
811 # Information about the format of the request is to be found
ebdb3724
CM
812 # https://www.duckdns.org/spec.jsp
813
814 url = "https://www.duckdns.org/update"
815 can_remove_records = False
816
817 def update(self):
818 # Raise an error if no auth details are given.
819 if not self.token:
820 raise DDNSConfigurationError
fc91be92 821
ebdb3724
CM
822 data = {
823 "domains" : self.hostname,
824 "token" : self.token,
825 }
826
827 # Check if we update an IPv4 address.
828 address4 = self.get_address("ipv4")
829 if address4:
830 data["ip"] = address4
831
832 # Check if we update an IPv6 address.
833 address6 = self.get_address("ipv6")
834 if address6:
835 data["ipv6"] = address6
836
837 # Raise an error if no address is given.
838 if "ip" not in data and "ipv6" not in data:
839 raise DDNSConfigurationError
840
841 # Send update to the server.
842 response = self.send_request(self.url, data=data)
843
844 # Get the full response message.
845 output = response.read().decode()
846
847 # Remove all leading and trailing whitespace.
848 output = output.strip()
849
850 # Handle success messages.
851 if output == "OK":
852 return
853
854 # The provider does not give detailed information
855 # if the update fails. Only a "KO" will be sent back.
856 if output == "KO":
857 raise DDNSUpdateError
858
859 # If we got here, some other update error happened.
860 raise DDNSUpdateError
fc91be92
MT
861
862
9db9ea25
MP
863class DDNSProviderDyFi(DDNSProtocolDynDNS2, DDNSProvider):
864 handle = "dy.fi"
865 name = "dy.fi"
866 website = "https://www.dy.fi/"
867 protocols = ("ipv4",)
868
869 # Information about the format of the request is to be found
870 # https://www.dy.fi/page/clients?lang=en
871 # https://www.dy.fi/page/specification?lang=en
872
ce6e977f 873 url = "https://www.dy.fi/nic/update"
9db9ea25
MP
874
875 # Please only send automatic updates when your IP address changes,
876 # or once per 5 to 6 days to refresh the address mapping (they will
877 # expire if not refreshed within 7 days).
878 holdoff_days = 6
879
880
5d4bec40 881class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
882 handle = "dyndns.org"
883 name = "Dyn"
884 website = "http://dyn.com/dns/"
885 protocols = ("ipv4",)
bfed6701
SS
886
887 # Information about the format of the request is to be found
888 # http://http://dyn.com/support/developers/api/perform-update/
889 # http://dyn.com/support/developers/api/return-codes/
b2b05ef3 890
bfed6701
SS
891 url = "https://members.dyndns.org/nic/update"
892
bfed6701 893
ea32ab26
DW
894class DDNSProviderDomainOffensive(DDNSProtocolDynDNS2, DDNSProvider):
895 handle = "do.de"
896 name = "Domain-Offensive"
897 website = "https://www.do.de/"
898 protocols = ("ipv6", "ipv4")
899
900 # Detailed information about the request and response codes
901 # are available on the providers webpage.
902 # https://www.do.de/wiki/FlexDNS_-_Entwickler
903
904 url = "https://ddns.do.de/"
905
ac8b9f48
CS
906class DDNSProviderDynUp(DDNSProvider):
907 handle = "dynup.de"
908 name = "DynUp.DE"
909 website = "http://dynup.de/"
910 protocols = ("ipv4",)
911
912 # Information about the format of the HTTPS request is to be found
913 # https://dyndnsfree.de/user/hilfe.php
914
915 url = "https://dynup.de/dyn.php"
916 can_remove_records = False
917
918 def update_protocol(self, proto):
919 data = {
920 "username" : self.username,
921 "password" : self.password,
922 "hostname" : self.hostname,
923 "print" : '1',
924 }
925
926 # Send update to the server.
927 response = self.send_request(self.url, data=data)
928
929 # Get the full response message.
472b2408 930 output = response.read().decode()
ac8b9f48
CS
931
932 # Remove all leading and trailing whitespace.
933 output = output.strip()
934
935 # Handle success messages.
91aead36 936 if output.startswith("I:OK"):
ac8b9f48
CS
937 return
938
939 # If we got here, some other update error happened.
940 raise DDNSUpdateError
941
942
5d4bec40 943class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
944 handle = "dynu.com"
945 name = "Dynu"
946 website = "http://dynu.com/"
947 protocols = ("ipv6", "ipv4",)
3a8407fa
SS
948
949 # Detailed information about the request and response codes
950 # are available on the providers webpage.
951 # http://dynu.com/Default.aspx?page=dnsapi
952
953 url = "https://api.dynu.com/nic/update"
954
d45139f6
MT
955 # DynU sends the IPv6 and IPv4 address in one request
956
957 def update(self):
958 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
54d3efc8
MT
959
960 # This one supports IPv6
cdc078dc
SS
961 myipv6 = self.get_address("ipv6")
962
963 # Add update information if we have an IPv6 address.
964 if myipv6:
965 data["myipv6"] = myipv6
54d3efc8 966
304026a3 967 self.send_request(data)
3a8407fa
SS
968
969
5420343a 970class DDNSProviderEasyDNS(DDNSProvider):
5d4bec40
MT
971 handle = "easydns.com"
972 name = "EasyDNS"
973 website = "http://www.easydns.com/"
974 protocols = ("ipv4",)
ee071271 975
5420343a
SS
976 # Detailed information about the request and response codes
977 # (API 1.3) are available on the providers webpage.
978 # https://fusion.easydns.com/index.php?/Knowledgebase/Article/View/102/7/dynamic-dns
ee071271
SS
979
980 url = "http://api.cp.easydns.com/dyn/tomato.php"
981
5420343a
SS
982 def update_protocol(self, proto):
983 data = {
984 "myip" : self.get_address(proto, "-"),
985 "hostname" : self.hostname,
986 }
987
988 # Send update to the server.
91aead36 989 response = self.send_request(self.url, data=data, username=self.username, password=self.password)
5420343a
SS
990
991 # Get the full response message.
472b2408 992 output = response.read().decode()
5420343a
SS
993
994 # Remove all leading and trailing whitespace.
995 output = output.strip()
996
997 # Handle success messages.
998 if output.startswith("NOERROR"):
999 return
1000
1001 # Handle error codes.
1002 if output.startswith("NOACCESS"):
1003 raise DDNSAuthenticationError
1004
1005 elif output.startswith("NOSERVICE"):
fb701db9 1006 raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain"))
5420343a
SS
1007
1008 elif output.startswith("ILLEGAL INPUT"):
fb701db9 1009 raise DDNSRequestError(_("Invalid data has been sent"))
5420343a
SS
1010
1011 elif output.startswith("TOOSOON"):
fb701db9 1012 raise DDNSRequestError(_("Too frequent update requests have been sent"))
5420343a
SS
1013
1014 # If we got here, some other update error happened.
1015 raise DDNSUpdateError
1016
ee071271 1017
90fe8843
CE
1018class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
1019 handle = "domopoli.de"
1020 name = "domopoli.de"
1021 website = "http://domopoli.de/"
1022 protocols = ("ipv4",)
1023
1024 # https://www.domopoli.de/?page=howto#DynDns_start
1025
1026 url = "http://dyndns.domopoli.de/nic/update"
1027
1028
a197d1a6
SS
1029class DDNSProviderDynsNet(DDNSProvider):
1030 handle = "dyns.net"
1031 name = "DyNS"
1032 website = "http://www.dyns.net/"
1033 protocols = ("ipv4",)
29c8c9c6 1034 can_remove_records = False
a197d1a6
SS
1035
1036 # There is very detailed informatio about how to send the update request and
1037 # the possible response codes. (Currently we are using the v1.1 proto)
1038 # http://www.dyns.net/documentation/technical/protocol/
1039
1040 url = "http://www.dyns.net/postscript011.php"
1041
d45139f6 1042 def update_protocol(self, proto):
a197d1a6 1043 data = {
d45139f6 1044 "ip" : self.get_address(proto),
a197d1a6
SS
1045 "host" : self.hostname,
1046 "username" : self.username,
1047 "password" : self.password,
1048 }
1049
1050 # Send update to the server.
1051 response = self.send_request(self.url, data=data)
1052
1053 # Get the full response message.
472b2408 1054 output = response.read().decode()
a197d1a6
SS
1055
1056 # Handle success messages.
1057 if output.startswith("200"):
1058 return
1059
1060 # Handle error codes.
1061 if output.startswith("400"):
fb701db9 1062 raise DDNSRequestError(_("Malformed request has been sent"))
a197d1a6
SS
1063 elif output.startswith("401"):
1064 raise DDNSAuthenticationError
1065 elif output.startswith("402"):
fb701db9 1066 raise DDNSRequestError(_("Too frequent update requests have been sent"))
a197d1a6
SS
1067 elif output.startswith("403"):
1068 raise DDNSInternalServerError
1069
1070 # If we got here, some other update error happened.
d8c43654 1071 raise DDNSUpdateError(_("Server response: %s") % output)
a197d1a6
SS
1072
1073
35216523
SS
1074class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
1075 handle = "enom.com"
1076 name = "eNom Inc."
1077 website = "http://www.enom.com/"
d45139f6 1078 protocols = ("ipv4",)
35216523
SS
1079
1080 # There are very detailed information about how to send an update request and
1081 # the respone codes.
1082 # http://www.enom.com/APICommandCatalog/
1083
1084 url = "https://dynamic.name-services.com/interface.asp"
29c8c9c6 1085 can_remove_records = False
35216523 1086
d45139f6 1087 def update_protocol(self, proto):
35216523
SS
1088 data = {
1089 "command" : "setdnshost",
1090 "responsetype" : "xml",
d45139f6 1091 "address" : self.get_address(proto),
35216523
SS
1092 "domainpassword" : self.password,
1093 "zone" : self.hostname
1094 }
1095
1096 # Send update to the server.
1097 response = self.send_request(self.url, data=data)
1098
1099 # Get the full response message.
472b2408 1100 output = response.read().decode()
35216523
SS
1101
1102 # Handle success messages.
1103 if self.get_xml_tag_value(output, "ErrCount") == "0":
1104 return
1105
1106 # Handle error codes.
1107 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
1108
1109 if errorcode == "304155":
1110 raise DDNSAuthenticationError
1111 elif errorcode == "304153":
fb701db9 1112 raise DDNSRequestError(_("Domain not found"))
35216523
SS
1113
1114 # If we got here, some other update error happened.
1115 raise DDNSUpdateError
1116
1117
ab4e352e
SS
1118class DDNSProviderEntryDNS(DDNSProvider):
1119 handle = "entrydns.net"
1120 name = "EntryDNS"
1121 website = "http://entrydns.net/"
1122 protocols = ("ipv4",)
1123
1124 # Some very tiny details about their so called "Simple API" can be found
1125 # here: https://entrydns.net/help
1126 url = "https://entrydns.net/records/modify"
29c8c9c6 1127 can_remove_records = False
ab4e352e 1128
d45139f6 1129 def update_protocol(self, proto):
ab4e352e 1130 data = {
d45139f6 1131 "ip" : self.get_address(proto),
ab4e352e
SS
1132 }
1133
1134 # Add auth token to the update url.
1135 url = "%s/%s" % (self.url, self.token)
1136
1137 # Send update to the server.
1138 try:
babc5e6d 1139 response = self.send_request(url, data=data)
ab4e352e
SS
1140
1141 # Handle error codes
91aead36 1142 except urllib.error.HTTPError as e:
ab4e352e
SS
1143 if e.code == 404:
1144 raise DDNSAuthenticationError
1145
1146 elif e.code == 422:
1147 raise DDNSRequestError(_("An invalid IP address was submitted"))
1148
1149 raise
1150
1151 # Handle success messages.
1152 if response.code == 200:
1153 return
1154
1155 # If we got here, some other update error happened.
1156 raise DDNSUpdateError
1157
1158
aa21a4c6 1159class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
6a11646e
MT
1160 handle = "freedns.afraid.org"
1161 name = "freedns.afraid.org"
1162 website = "http://freedns.afraid.org/"
aa21a4c6
SS
1163
1164 # No information about the request or response could be found on the vendor
1165 # page. All used values have been collected by testing.
1166 url = "https://freedns.afraid.org/dynamic/update.php"
29c8c9c6 1167 can_remove_records = False
aa21a4c6 1168
d45139f6 1169 def update_protocol(self, proto):
aa21a4c6 1170 data = {
d45139f6 1171 "address" : self.get_address(proto),
aa21a4c6
SS
1172 }
1173
1174 # Add auth token to the update url.
1175 url = "%s?%s" % (self.url, self.token)
1176
1177 # Send update to the server.
1178 response = self.send_request(url, data=data)
1179
a204b107 1180 # Get the full response message.
472b2408 1181 output = response.read().decode()
a204b107
SS
1182
1183 # Handle success messages.
aa21a4c6
SS
1184 if output.startswith("Updated") or "has not changed" in output:
1185 return
1186
1187 # Handle error codes.
1188 if output == "ERROR: Unable to locate this record":
1189 raise DDNSAuthenticationError
1190 elif "is an invalid IP address" in output:
fb701db9 1191 raise DDNSRequestError(_("Invalid IP address has been sent"))
aa21a4c6 1192
3b524cf2
SS
1193 # If we got here, some other update error happened.
1194 raise DDNSUpdateError
1195
aa21a4c6 1196
b39971d1
AK
1197class DDNSProviderItsdns(DDNSProtocolDynDNS2, DDNSProvider):
1198 handle = "inwx.com"
1199 name = "INWX"
1200 website = "https://www.inwx.com"
1201 protocols = ("ipv6", "ipv4")
1202
1203 # Information about the format of the HTTP request is to be found
1204 # here: https://www.inwx.com/en/nameserver2/dyndns (requires login)
1205 # Notice: The URL is the same for: inwx.com|de|at|ch|es
1206
1207 url = "https://dyndns.inwx.com/nic/update"
1208
1209
327095f0
JS
1210class DDNSProviderItsdns(DDNSProtocolDynDNS2, DDNSProvider):
1211 handle = "itsdns.de"
1212 name = "it's DNS"
1213 website = "http://www.itsdns.de/"
1214 protocols = ("ipv6", "ipv4")
1215
1216 # Information about the format of the HTTP request is to be found
1217 # here: https://www.itsdns.de/dynupdatehelp.htm
1218
1219 url = "https://www.itsdns.de/update.php"
1220
1221
09b07b23
LAH
1222class DDNSProviderJoker(DDNSProtocolDynDNS2, DDNSProvider):
1223 handle = "joker.com"
1224 name = "Joker.com Dynamic DNS"
1225 website = "https://joker.com/"
1226 protocols = ("ipv4",)
1227
1228 # Information about the request can be found here:
1229 # https://joker.com/faq/content/11/427/en/what-is-dynamic-dns-dyndns.html
1230 # Using DynDNS V2 protocol over HTTPS here
1231
1232 url = "https://svc.joker.com/nic/update"
1233
1234
f1c737c0
CW
1235class DDNSProviderKEYSYSTEMS(DDNSProvider):
1236 handle = "key-systems.net"
1237 name = "dynamicdns.key-systems.net"
1238 website = "https://domaindiscount24.com/"
1239 protocols = ("ipv4",)
1240
1241 # There are only information provided by the domaindiscount24 how to
1242 # perform an update with HTTP APIs
1243 # https://www.domaindiscount24.com/faq/dynamic-dns
1244 # examples: https://dynamicdns.key-systems.net/update.php?hostname=hostname&password=password&ip=auto
1245 # https://dynamicdns.key-systems.net/update.php?hostname=hostname&password=password&ip=213.x.x.x&mx=213.x.x.x
1246
1247 url = "https://dynamicdns.key-systems.net/update.php"
1248 can_remove_records = False
1249
1250 def update_protocol(self, proto):
1251 address = self.get_address(proto)
1252 data = {
1253 "hostname" : self.hostname,
1254 "password" : self.password,
1255 "ip" : address,
1256 }
1257
1258 # Send update to the server.
1259 response = self.send_request(self.url, data=data)
1260
1261 # Get the full response message.
1262 output = response.read().decode()
1263
1264 # Handle success messages.
1265 if "code = 200" in output:
1266 return
1267
1268 # Handle error messages.
1269 if "abuse prevention triggered" in output:
1270 raise DDNSAbuseError
1271 elif "invalid password" in output:
1272 raise DDNSAuthenticationError
1273 elif "Authorization failed" in output:
1274 raise DDNSRequestError(_("Invalid hostname specified"))
1275
1276 # If we got here, some other update error happened.
1277 raise DDNSUpdateError
1278
1279
c510004d
SS
1280class DDNSProviderGoogle(DDNSProtocolDynDNS2, DDNSProvider):
1281 handle = "domains.google.com"
1282 name = "Google Domains"
1283 website = "https://domains.google.com/"
1284 protocols = ("ipv4",)
1285
1286 # Information about the format of the HTTP request is to be found
1287 # here: https://support.google.com/domains/answer/6147083?hl=en
1288
1289 url = "https://domains.google.com/nic/update"
1290
1291
a08c1b72 1292class DDNSProviderLightningWireLabs(DDNSProvider):
6a11646e 1293 handle = "dns.lightningwirelabs.com"
fb115fdc 1294 name = "Lightning Wire Labs DNS Service"
92cd7211 1295 website = "https://dns.lightningwirelabs.com/"
a08c1b72
SS
1296
1297 # Information about the format of the HTTPS request is to be found
1298 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
b2b05ef3 1299
a08c1b72
SS
1300 url = "https://dns.lightningwirelabs.com/update"
1301
5f402f36 1302 def update(self):
c772f5f2
MT
1303 # Raise an error if no auth details are given.
1304 if not self.token:
1305 raise DDNSConfigurationError
1306
a08c1b72
SS
1307 data = {
1308 "hostname" : self.hostname,
c772f5f2 1309 "token" : self.token,
e3c70807
MT
1310 "address6" : self.get_address("ipv6", "-"),
1311 "address4" : self.get_address("ipv4", "-"),
a08c1b72
SS
1312 }
1313
a08c1b72 1314 # Send update to the server.
cb455540 1315 response = self.send_request(self.url, data=data)
a08c1b72
SS
1316
1317 # Handle success messages.
1318 if response.code == 200:
1319 return
1320
a08c1b72
SS
1321 # If we got here, some other update error happened.
1322 raise DDNSUpdateError
1323
1324
9d3cfba8
SS
1325class DDNSProviderLoopia(DDNSProtocolDynDNS2, DDNSProvider):
1326 handle = "loopia.se"
1327 name = "Loopia AB"
1328 website = "https://www.loopia.com"
1329 protocols = ("ipv4",)
1330
1331 # Information about the format of the HTTP request is to be found
1332 # here: https://support.loopia.com/wiki/About_the_DynDNS_support
1333
1334 url = "https://dns.loopia.se/XDynDNSServer/XDynDNS.php"
1335
1336
446e42af
GH
1337class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
1338 handle = "myonlineportal.net"
1339 name = "myonlineportal.net"
1340 website = "https:/myonlineportal.net/"
1341
1342 # Information about the request and response can be obtained here:
1343 # https://myonlineportal.net/howto_dyndns
1344
1345 url = "https://myonlineportal.net/updateddns"
1346
1347 def prepare_request_data(self, proto):
1348 data = {
1349 "hostname" : self.hostname,
1350 "ip" : self.get_address(proto),
1351 }
1352
1353 return data
1354
1355
78c9780b 1356class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
6a11646e
MT
1357 handle = "namecheap.com"
1358 name = "Namecheap"
1359 website = "http://namecheap.com"
1360 protocols = ("ipv4",)
d1cd57eb
SS
1361
1362 # Information about the format of the HTTP request is to be found
1363 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
1364 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
1365
1366 url = "https://dynamicdns.park-your-domain.com/update"
29c8c9c6 1367 can_remove_records = False
d1cd57eb 1368
d45139f6 1369 def update_protocol(self, proto):
d1cd57eb
SS
1370 # Namecheap requires the hostname splitted into a host and domain part.
1371 host, domain = self.hostname.split(".", 1)
1372
47ea9f41
SS
1373 # Get and store curent IP address.
1374 address = self.get_address(proto)
1375
d1cd57eb 1376 data = {
47ea9f41 1377 "ip" : address,
d1cd57eb
SS
1378 "password" : self.password,
1379 "host" : host,
1380 "domain" : domain
1381 }
1382
1383 # Send update to the server.
1384 response = self.send_request(self.url, data=data)
1385
1386 # Get the full response message.
472b2408 1387 output = response.read().decode()
d1cd57eb
SS
1388
1389 # Handle success messages.
d45139f6 1390 if self.get_xml_tag_value(output, "IP") == address:
d1cd57eb
SS
1391 return
1392
1393 # Handle error codes.
78c9780b 1394 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
d1cd57eb
SS
1395
1396 if errorcode == "304156":
1397 raise DDNSAuthenticationError
1398 elif errorcode == "316153":
fb701db9 1399 raise DDNSRequestError(_("Domain not found"))
d1cd57eb 1400 elif errorcode == "316154":
fb701db9 1401 raise DDNSRequestError(_("Domain not active"))
d1cd57eb
SS
1402 elif errorcode in ("380098", "380099"):
1403 raise DDNSInternalServerError
1404
1405 # If we got here, some other update error happened.
1406 raise DDNSUpdateError
1407
1408
5d4bec40 1409class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
c0277eee 1410 handle = "no-ip.com"
bfdba55a
SS
1411 name = "NoIP"
1412 website = "http://www.noip.com/"
5d4bec40 1413 protocols = ("ipv4",)
f22ab085
MT
1414
1415 # Information about the format of the HTTP request is to be found
bfdba55a
SS
1416 # here: http://www.noip.com/integrate/request and
1417 # here: http://www.noip.com/integrate/response
f22ab085 1418
bfdba55a 1419 url = "http://dynupdate.noip.com/nic/update"
2de06f59 1420
d45139f6
MT
1421 def prepare_request_data(self, proto):
1422 assert proto == "ipv4"
1423
2de06f59
MT
1424 data = {
1425 "hostname" : self.hostname,
d45139f6 1426 "address" : self.get_address(proto),
f22ab085
MT
1427 }
1428
88f39629 1429 return data
f22ab085
MT
1430
1431
c6e78218
SS
1432class DDNSProviderNowDNS(DDNSProtocolDynDNS2, DDNSProvider):
1433 handle = "now-dns.com"
1434 name = "NOW-DNS"
1435 website = "http://now-dns.com/"
1436 protocols = ("ipv6", "ipv4")
1437
1438 # Information about the format of the request is to be found
1439 # but only can be accessed by register an account and login
1440 # https://now-dns.com/?m=api
1441
1442 url = "https://now-dns.com/update"
1443
1444
31c95e4b
SS
1445class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
1446 handle = "nsupdate.info"
1447 name = "nsupdate.info"
b9221322 1448 website = "http://nsupdate.info/"
31c95e4b
SS
1449 protocols = ("ipv6", "ipv4",)
1450
1451 # Information about the format of the HTTP request can be found
b9221322 1452 # after login on the provider user interface and here:
31c95e4b
SS
1453 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1454
7b5d382e
SS
1455 url = "https://nsupdate.info/nic/update"
1456
29c8c9c6
MT
1457 # TODO nsupdate.info can actually do this, but the functionality
1458 # has not been implemented here, yet.
1459 can_remove_records = False
1460
64018439
MT
1461 # After a failed update, there will be no retries
1462 # https://bugzilla.ipfire.org/show_bug.cgi?id=10603
1463 holdoff_failure_days = None
1464
31c95e4b
SS
1465 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1466 # and for the password a so called secret.
1467 @property
1468 def username(self):
1469 return self.get("hostname")
1470
1471 @property
1472 def password(self):
9c777232 1473 return self.token or self.get("secret")
31c95e4b 1474
d45139f6 1475 def prepare_request_data(self, proto):
31c95e4b 1476 data = {
d45139f6 1477 "myip" : self.get_address(proto),
31c95e4b
SS
1478 }
1479
1480 return data
1481
1482
90663439
SS
1483class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
1484 handle = "opendns.com"
1485 name = "OpenDNS"
1486 website = "http://www.opendns.com"
1487
1488 # Detailed information about the update request and possible
1489 # response codes can be obtained from here:
1490 # https://support.opendns.com/entries/23891440
1491
1492 url = "https://updates.opendns.com/nic/update"
1493
d45139f6 1494 def prepare_request_data(self, proto):
90663439
SS
1495 data = {
1496 "hostname" : self.hostname,
d45139f6 1497 "myip" : self.get_address(proto),
90663439
SS
1498 }
1499
1500 return data
1501
1502
5d4bec40
MT
1503class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1504 handle = "ovh.com"
1505 name = "OVH"
1506 website = "http://www.ovh.com/"
1507 protocols = ("ipv4",)
a508bda6
SS
1508
1509 # OVH only provides very limited information about how to
1510 # update a DynDNS host. They only provide the update url
1511 # on the their german subpage.
1512 #
1513 # http://hilfe.ovh.de/DomainDynHost
1514
1515 url = "https://www.ovh.com/nic/update"
1516
d45139f6
MT
1517 def prepare_request_data(self, proto):
1518 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
54d3efc8
MT
1519 data.update({
1520 "system" : "dyndns",
1521 })
1522
1523 return data
a508bda6
SS
1524
1525
ef33455e 1526class DDNSProviderRegfish(DDNSProvider):
6a11646e
MT
1527 handle = "regfish.com"
1528 name = "Regfish GmbH"
1529 website = "http://www.regfish.com/"
ef33455e
SS
1530
1531 # A full documentation to the providers api can be found here
1532 # but is only available in german.
1533 # https://www.regfish.de/domains/dyndns/dokumentation
1534
1535 url = "https://dyndns.regfish.de/"
29c8c9c6 1536 can_remove_records = False
ef33455e
SS
1537
1538 def update(self):
1539 data = {
1540 "fqdn" : self.hostname,
1541 }
1542
1543 # Check if we update an IPv6 address.
1544 address6 = self.get_address("ipv6")
1545 if address6:
1546 data["ipv6"] = address6
1547
1548 # Check if we update an IPv4 address.
1549 address4 = self.get_address("ipv4")
1550 if address4:
1551 data["ipv4"] = address4
1552
1553 # Raise an error if none address is given.
91aead36 1554 if "ipv6" not in data and "ipv4" not in data:
ef33455e
SS
1555 raise DDNSConfigurationError
1556
1557 # Check if a token has been set.
1558 if self.token:
1559 data["token"] = self.token
1560
1561 # Raise an error if no token and no useranem and password
1562 # are given.
1563 elif not self.username and not self.password:
fb701db9 1564 raise DDNSConfigurationError(_("No Auth details specified"))
ef33455e
SS
1565
1566 # HTTP Basic Auth is only allowed if no token is used.
1567 if self.token:
1568 # Send update to the server.
1569 response = self.send_request(self.url, data=data)
1570 else:
1571 # Send update to the server.
91aead36 1572 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
ef33455e
SS
1573
1574 # Get the full response message.
472b2408 1575 output = response.read().decode()
ef33455e
SS
1576
1577 # Handle success messages.
1578 if "100" in output or "101" in output:
1579 return
1580
1581 # Handle error codes.
1582 if "401" or "402" in output:
1583 raise DDNSAuthenticationError
1584 elif "408" in output:
fb701db9 1585 raise DDNSRequestError(_("Invalid IPv4 address has been sent"))
ef33455e 1586 elif "409" in output:
fb701db9 1587 raise DDNSRequestError(_("Invalid IPv6 address has been sent"))
ef33455e 1588 elif "412" in output:
fb701db9 1589 raise DDNSRequestError(_("No valid FQDN was given"))
ef33455e
SS
1590 elif "414" in output:
1591 raise DDNSInternalServerError
1592
1593 # If we got here, some other update error happened.
1594 raise DDNSUpdateError
1595
1596
f77e6bc9
SS
1597class DDNSProviderSchokokeksDNS(DDNSProtocolDynDNS2, DDNSProvider):
1598 handle = "schokokeks.org"
1599 name = "Schokokeks"
1600 website = "http://www.schokokeks.org/"
1601 protocols = ("ipv4",)
1602
1603 # Information about the format of the request is to be found
1604 # https://wiki.schokokeks.org/DynDNS
1605 url = "https://dyndns.schokokeks.org/nic/update"
1606
1607
5d4bec40 1608class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
1609 handle = "selfhost.de"
1610 name = "Selfhost.de"
1611 website = "http://www.selfhost.de/"
5d4bec40 1612 protocols = ("ipv4",)
f22ab085 1613
04db1862 1614 url = "https://carol.selfhost.de/nic/update"
f22ab085 1615
d45139f6
MT
1616 def prepare_request_data(self, proto):
1617 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
04db1862
MT
1618 data.update({
1619 "hostname" : "1",
1620 })
f22ab085 1621
04db1862 1622 return data
b09b1545
SS
1623
1624
52e8a969
JS
1625class DDNSProviderServercow(DDNSProvider):
1626 handle = "servercow.de"
1627 name = "servercow.de"
1628 website = "https://servercow.de/"
1629 protocols = ("ipv4", "ipv6")
1630
1631 url = "https://www.servercow.de/dnsupdate/update.php"
1632 can_remove_records = False
1633
1634 def update_protocol(self, proto):
1635 data = {
1636 "ipaddr" : self.get_address(proto),
1637 "hostname" : self.hostname,
1638 "username" : self.username,
1639 "pass" : self.password,
1640 }
1641
1642 # Send request to provider
1643 response = self.send_request(self.url, data=data)
1644
1645 # Read response
472b2408 1646 output = response.read().decode()
52e8a969
JS
1647
1648 # Server responds with OK if update was successful
1649 if output.startswith("OK"):
1650 return
1651
1652 # Catch any errors
1653 elif output.startswith("FAILED - Authentication failed"):
1654 raise DDNSAuthenticationError
1655
1656 # If we got here, some other update error happened
1657 raise DDNSUpdateError(output)
1658
1659
5d4bec40
MT
1660class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1661 handle = "spdns.org"
74703885
MT
1662 name = "SPDYN"
1663 website = "https://www.spdyn.de/"
b09b1545
SS
1664
1665 # Detailed information about request and response codes are provided
1666 # by the vendor. They are using almost the same mechanism and status
1667 # codes as dyndns.org so we can inherit all those stuff.
1668 #
1669 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1670 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1671
74703885 1672 url = "https://update.spdyn.de/nic/update"
4ec90b93 1673
94ab4379
SS
1674 @property
1675 def username(self):
1676 return self.get("username") or self.hostname
1677
1678 @property
1679 def password(self):
25f39b4e 1680 return self.get("password") or self.token
94ab4379 1681
4ec90b93 1682
5d4bec40
MT
1683class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1684 handle = "strato.com"
1685 name = "Strato AG"
1686 website = "http:/www.strato.com/"
1687 protocols = ("ipv4",)
7488825c
SS
1688
1689 # Information about the request and response can be obtained here:
1690 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1691
1692 url = "https://dyndns.strato.com/nic/update"
1693
34af066e
SS
1694 def prepare_request_data(self, proto):
1695 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1696 data.update({
1697 "mx" : "NOCHG",
1698 "backupmx" : "NOCHG"
1699 })
1700
1701 return data
1702
7488825c 1703
5d4bec40
MT
1704class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1705 handle = "twodns.de"
1706 name = "TwoDNS"
1707 website = "http://www.twodns.de"
1708 protocols = ("ipv4",)
a6183090
SS
1709
1710 # Detailed information about the request can be found here
1711 # http://twodns.de/en/faqs
1712 # http://twodns.de/en/api
1713
1714 url = "https://update.twodns.de/update"
1715
d45139f6
MT
1716 def prepare_request_data(self, proto):
1717 assert proto == "ipv4"
1718
a6183090 1719 data = {
d45139f6 1720 "ip" : self.get_address(proto),
a6183090
SS
1721 "hostname" : self.hostname
1722 }
1723
1724 return data
1725
1726
5d4bec40
MT
1727class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1728 handle = "udmedia.de"
1729 name = "Udmedia GmbH"
1730 website = "http://www.udmedia.de"
1731 protocols = ("ipv4",)
03bdd188
SS
1732
1733 # Information about the request can be found here
1734 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1735
1736 url = "https://www.udmedia.de/nic/update"
1737
1738
5d4bec40 1739class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
1740 handle = "variomedia.de"
1741 name = "Variomedia"
1742 website = "http://www.variomedia.de/"
1743 protocols = ("ipv6", "ipv4",)
c8c7ca8f
SS
1744
1745 # Detailed information about the request can be found here
1746 # https://dyndns.variomedia.de/
1747
1748 url = "https://dyndns.variomedia.de/nic/update"
1749
d45139f6 1750 def prepare_request_data(self, proto):
c8c7ca8f
SS
1751 data = {
1752 "hostname" : self.hostname,
d45139f6 1753 "myip" : self.get_address(proto),
c8c7ca8f 1754 }
54d3efc8
MT
1755
1756 return data
98fbe467
SS
1757
1758
6b81b43c
MB
1759class DDNSProviderXLhost(DDNSProtocolDynDNS2, DDNSProvider):
1760 handle = "xlhost.de"
1761 name = "XLhost"
1762 website = "http://xlhost.de/"
1763 protocols = ("ipv4",)
1764
1765 # Information about the format of the HTTP request is to be found
1766 # here: https://xlhost.de/faq/index_html?topicId=CQA2ELIPO4SQ
1767
1768 url = "https://nsupdate.xlhost.de/"
1769
1770
f0554226 1771class DDNSProviderZoneedit(DDNSProvider):
5d4bec40
MT
1772 handle = "zoneedit.com"
1773 name = "Zoneedit"
1774 website = "http://www.zoneedit.com"
1775 protocols = ("ipv4",)
98fbe467
SS
1776
1777 # Detailed information about the request and the response codes can be
1778 # obtained here:
1779 # http://www.zoneedit.com/doc/api/other.html
1780 # http://www.zoneedit.com/faq.html
1781
1782 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1783
d45139f6 1784 def update_protocol(self, proto):
98fbe467 1785 data = {
d45139f6 1786 "dnsto" : self.get_address(proto),
98fbe467
SS
1787 "host" : self.hostname
1788 }
1789
1790 # Send update to the server.
91aead36 1791 response = self.send_request(self.url, username=self.username, password=self.password, data=data)
98fbe467
SS
1792
1793 # Get the full response message.
472b2408 1794 output = response.read().decode()
98fbe467
SS
1795
1796 # Handle success messages.
1797 if output.startswith("<SUCCESS"):
1798 return
1799
1800 # Handle error codes.
1801 if output.startswith("invalid login"):
1802 raise DDNSAuthenticationError
1803 elif output.startswith("<ERROR CODE=\"704\""):
fb701db9 1804 raise DDNSRequestError(_("No valid FQDN was given"))
98fbe467 1805 elif output.startswith("<ERROR CODE=\"702\""):
fb701db9 1806 raise DDNSRequestError(_("Too frequent update requests have been sent"))
98fbe467
SS
1807
1808 # If we got here, some other update error happened.
1809 raise DDNSUpdateError
e53d3225
SS
1810
1811
d1cb1b54
MG
1812class DDNSProviderDNSmadeEasy(DDNSProvider):
1813 handle = "dnsmadeeasy.com"
1814 name = "DNSmadeEasy.com"
1815 website = "http://www.dnsmadeeasy.com/"
1816 protocols = ("ipv4",)
1817
1818 # DNS Made Easy Nameserver Provider also offering Dynamic DNS
1819 # Documentation can be found here:
1820 # http://www.dnsmadeeasy.com/dynamic-dns/
1821
1822 url = "https://cp.dnsmadeeasy.com/servlet/updateip?"
1823 can_remove_records = False
1824
1825 def update_protocol(self, proto):
1826 data = {
1827 "ip" : self.get_address(proto),
1828 "id" : self.hostname,
1829 "username" : self.username,
1830 "password" : self.password,
1831 }
1832
1833 # Send update to the server.
1834 response = self.send_request(self.url, data=data)
1835
1836 # Get the full response message.
472b2408 1837 output = response.read().decode()
d1cb1b54
MG
1838
1839 # Handle success messages.
1840 if output.startswith("success") or output.startswith("error-record-ip-same"):
1841 return
1842
1843 # Handle error codes.
1844 if output.startswith("error-auth-suspend"):
fb701db9 1845 raise DDNSRequestError(_("Account has been suspended"))
d1cb1b54
MG
1846
1847 elif output.startswith("error-auth-voided"):
fb701db9 1848 raise DDNSRequestError(_("Account has been revoked"))
d1cb1b54
MG
1849
1850 elif output.startswith("error-record-invalid"):
fb701db9 1851 raise DDNSRequestError(_("Specified host does not exist"))
d1cb1b54
MG
1852
1853 elif output.startswith("error-auth"):
1854 raise DDNSAuthenticationError
1855
1856 # If we got here, some other update error happened.
1857 raise DDNSUpdateError(_("Server response: %s") % output)
1858
1859
e53d3225
SS
1860class DDNSProviderZZZZ(DDNSProvider):
1861 handle = "zzzz.io"
1862 name = "zzzz"
1863 website = "https://zzzz.io"
fbdff678 1864 protocols = ("ipv6", "ipv4",)
e53d3225
SS
1865
1866 # Detailed information about the update request can be found here:
1867 # https://zzzz.io/faq/
1868
1869 # Details about the possible response codes have been provided in the bugtracker:
1870 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1871
1872 url = "https://zzzz.io/api/v1/update"
29c8c9c6 1873 can_remove_records = False
e53d3225 1874
d45139f6 1875 def update_protocol(self, proto):
e53d3225 1876 data = {
d45139f6 1877 "ip" : self.get_address(proto),
e53d3225
SS
1878 "token" : self.token,
1879 }
1880
fbdff678
MT
1881 if proto == "ipv6":
1882 data["type"] = "aaaa"
1883
e53d3225
SS
1884 # zzzz uses the host from the full hostname as part
1885 # of the update url.
1886 host, domain = self.hostname.split(".", 1)
1887
1888 # Add host value to the update url.
1889 url = "%s/%s" % (self.url, host)
1890
1891 # Send update to the server.
1892 try:
1893 response = self.send_request(url, data=data)
1894
1895 # Handle error codes.
ff43fa70
MT
1896 except DDNSNotFound:
1897 raise DDNSRequestError(_("Invalid hostname specified"))
e53d3225
SS
1898
1899 # Handle success messages.
1900 if response.code == 200:
1901 return
1902
1903 # If we got here, some other update error happened.
1904 raise DDNSUpdateError