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