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