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