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