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