e708fd648139b552a0ecc93aa8c18d771ae06ef7
[ddns.git] / src / ddns / providers.py
1 #!/usr/bin/python
2 ###############################################################################
3 #                                                                             #
4 # ddns - A dynamic DNS client for IPFire                                      #
5 # Copyright (C) 2012 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 DDNSProviderDDNSS(DDNSProvider):
590         handle    = "ddnss.de"
591         name      = "DDNSS"
592         website   = "http://www.ddnss.de"
593         protocols = ("ipv4",)
594
595         # Detailed information about how to send the update request and possible response
596         # codes can be obtained from here.
597         # http://www.ddnss.de/info.php
598         # http://www.megacomputing.de/2014/08/dyndns-service-response-time/#more-919
599
600         url = "http://www.ddnss.de/upd.php"
601         can_remove_records = False
602
603         def update_protocol(self, proto):
604                 data = {
605                         "ip"   : self.get_address(proto),
606                         "host" : self.hostname,
607                 }
608
609                 # Check if a token has been set.
610                 if self.token:
611                         data["key"] = self.token
612
613                 # Check if username and hostname are given.
614                 elif self.username and self.password:
615                         data.update({
616                                 "user" : self.username,
617                                 "pwd"  : self.password,
618                         })
619
620                 # Raise an error if no auth details are given.
621                 else:
622                         raise DDNSConfigurationError
623
624                 # Send update to the server.
625                 response = self.send_request(self.url, data=data)
626
627                 # This provider sends the response code as part of the header.
628                 header = response.info()
629
630                 # Get status information from the header.
631                 output = header.getheader('ddnss-response')
632
633                 # Handle success messages.
634                 if output == "good" or output == "nochg":
635                         return
636
637                 # Handle error codes.
638                 if output == "badauth":
639                         raise DDNSAuthenticationError
640                 elif output == "notfqdn":
641                         raise DDNSRequestError(_("No valid FQDN was given."))
642                 elif output == "nohost":
643                         raise DDNSRequestError(_("Specified host does not exist."))
644                 elif output == "911":
645                         raise DDNSInternalServerError
646                 elif output == "dnserr":
647                         raise DDNSInternalServerError(_("DNS error encountered."))
648                 elif output == "disabled":
649                         raise DDNSRequestError(_("Account disabled or locked."))
650
651                 # If we got here, some other update error happened.
652                 raise DDNSUpdateError
653
654
655 class DDNSProviderDHS(DDNSProvider):
656         handle    = "dhs.org"
657         name      = "DHS International"
658         website   = "http://dhs.org/"
659         protocols = ("ipv4",)
660
661         # No information about the used update api provided on webpage,
662         # grabed from source code of ez-ipudate.
663
664         url = "http://members.dhs.org/nic/hosts"
665         can_remove_records = False
666
667         def update_protocol(self, proto):
668                 data = {
669                         "domain"       : self.hostname,
670                         "ip"           : self.get_address(proto),
671                         "hostcmd"      : "edit",
672                         "hostcmdstage" : "2",
673                         "type"         : "4",
674                 }
675
676                 # Send update to the server.
677                 response = self.send_request(self.url, username=self.username, password=self.password,
678                         data=data)
679
680                 # Handle success messages.
681                 if response.code == 200:
682                         return
683
684                 # If we got here, some other update error happened.
685                 raise DDNSUpdateError
686
687
688 class DDNSProviderDNSpark(DDNSProvider):
689         handle    = "dnspark.com"
690         name      = "DNS Park"
691         website   = "http://dnspark.com/"
692         protocols = ("ipv4",)
693
694         # Informations to the used api can be found here:
695         # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
696
697         url = "https://control.dnspark.com/api/dynamic/update.php"
698         can_remove_records = False
699
700         def update_protocol(self, proto):
701                 data = {
702                         "domain" : self.hostname,
703                         "ip"     : self.get_address(proto),
704                 }
705
706                 # Send update to the server.
707                 response = self.send_request(self.url, username=self.username, password=self.password,
708                         data=data)
709
710                 # Get the full response message.
711                 output = response.read()
712
713                 # Handle success messages.
714                 if output.startswith("ok") or output.startswith("nochange"):
715                         return
716
717                 # Handle error codes.
718                 if output == "unauth":
719                         raise DDNSAuthenticationError
720                 elif output == "abuse":
721                         raise DDNSAbuseError
722                 elif output == "blocked":
723                         raise DDNSBlockedError
724                 elif output == "nofqdn":
725                         raise DDNSRequestError(_("No valid FQDN was given."))
726                 elif output == "nohost":
727                         raise DDNSRequestError(_("Invalid hostname specified."))
728                 elif output == "notdyn":
729                         raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
730                 elif output == "invalid":
731                         raise DDNSRequestError(_("Invalid IP address has been sent."))
732
733                 # If we got here, some other update error happened.
734                 raise DDNSUpdateError
735
736
737 class DDNSProviderDtDNS(DDNSProvider):
738         handle    = "dtdns.com"
739         name      = "DtDNS"
740         website   = "http://dtdns.com/"
741         protocols = ("ipv4",)
742
743         # Information about the format of the HTTPS request is to be found
744         # http://www.dtdns.com/dtsite/updatespec
745
746         url = "https://www.dtdns.com/api/autodns.cfm"
747         can_remove_records = False
748
749         def update_protocol(self, proto):
750                 data = {
751                         "ip" : self.get_address(proto),
752                         "id" : self.hostname,
753                         "pw" : self.password
754                 }
755
756                 # Send update to the server.
757                 response = self.send_request(self.url, data=data)
758
759                 # Get the full response message.
760                 output = response.read()
761
762                 # Remove all leading and trailing whitespace.
763                 output = output.strip()
764
765                 # Handle success messages.
766                 if "now points to" in output:
767                         return
768
769                 # Handle error codes.
770                 if output == "No hostname to update was supplied.":
771                         raise DDNSRequestError(_("No hostname specified."))
772
773                 elif output == "The hostname you supplied is not valid.":
774                         raise DDNSRequestError(_("Invalid hostname specified."))
775
776                 elif output == "The password you supplied is not valid.":
777                         raise DDNSAuthenticationError
778
779                 elif output == "Administration has disabled this account.":
780                         raise DDNSRequestError(_("Account has been disabled."))
781
782                 elif output == "Illegal character in IP.":
783                         raise DDNSRequestError(_("Invalid IP address has been sent."))
784
785                 elif output == "Too many failed requests.":
786                         raise DDNSRequestError(_("Too many failed requests."))
787
788                 # If we got here, some other update error happened.
789                 raise DDNSUpdateError
790
791
792 class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
793         handle    = "dyndns.org"
794         name      = "Dyn"
795         website   = "http://dyn.com/dns/"
796         protocols = ("ipv4",)
797
798         # Information about the format of the request is to be found
799         # http://http://dyn.com/support/developers/api/perform-update/
800         # http://dyn.com/support/developers/api/return-codes/
801
802         url = "https://members.dyndns.org/nic/update"
803
804
805 class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
806         handle    = "dynu.com"
807         name      = "Dynu"
808         website   = "http://dynu.com/"
809         protocols = ("ipv6", "ipv4",)
810
811         # Detailed information about the request and response codes
812         # are available on the providers webpage.
813         # http://dynu.com/Default.aspx?page=dnsapi
814
815         url = "https://api.dynu.com/nic/update"
816
817         # DynU sends the IPv6 and IPv4 address in one request
818
819         def update(self):
820                 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
821
822                 # This one supports IPv6
823                 myipv6 = self.get_address("ipv6")
824
825                 # Add update information if we have an IPv6 address.
826                 if myipv6:
827                         data["myipv6"] = myipv6
828
829                 self.send_request(data)
830
831
832 class DDNSProviderEasyDNS(DDNSProvider):
833         handle    = "easydns.com"
834         name      = "EasyDNS"
835         website   = "http://www.easydns.com/"
836         protocols = ("ipv4",)
837
838         # Detailed information about the request and response codes
839         # (API 1.3) are available on the providers webpage.
840         # https://fusion.easydns.com/index.php?/Knowledgebase/Article/View/102/7/dynamic-dns
841
842         url = "http://api.cp.easydns.com/dyn/tomato.php"
843
844         def update_protocol(self, proto):
845                 data = {
846                         "myip"     : self.get_address(proto, "-"),
847                         "hostname" : self.hostname,
848                 }
849
850                 # Send update to the server.
851                 response = self.send_request(self.url, data=data,
852                         username=self.username, password=self.password)
853
854                 # Get the full response message.
855                 output = response.read()
856
857                 # Remove all leading and trailing whitespace.
858                 output = output.strip()
859
860                 # Handle success messages.
861                 if output.startswith("NOERROR"):
862                         return
863
864                 # Handle error codes.
865                 if output.startswith("NOACCESS"):
866                         raise DDNSAuthenticationError
867
868                 elif output.startswith("NOSERVICE"):
869                         raise DDNSRequestError(_("Dynamic DNS is not turned on for this domain."))
870
871                 elif output.startswith("ILLEGAL INPUT"):
872                         raise DDNSRequestError(_("Invalid data has been sent."))
873
874                 elif output.startswith("TOOSOON"):
875                         raise DDNSRequestError(_("Too frequent update requests have been sent."))
876
877                 # If we got here, some other update error happened.
878                 raise DDNSUpdateError
879
880
881 class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
882         handle    = "domopoli.de"
883         name      = "domopoli.de"
884         website   = "http://domopoli.de/"
885         protocols = ("ipv4",)
886
887         # https://www.domopoli.de/?page=howto#DynDns_start
888
889         url = "http://dyndns.domopoli.de/nic/update"
890
891
892 class DDNSProviderDynsNet(DDNSProvider):
893         handle    = "dyns.net"
894         name      = "DyNS"
895         website   = "http://www.dyns.net/"
896         protocols = ("ipv4",)
897         can_remove_records = False
898
899         # There is very detailed informatio about how to send the update request and
900         # the possible response codes. (Currently we are using the v1.1 proto)
901         # http://www.dyns.net/documentation/technical/protocol/
902
903         url = "http://www.dyns.net/postscript011.php"
904
905         def update_protocol(self, proto):
906                 data = {
907                         "ip"       : self.get_address(proto),
908                         "host"     : self.hostname,
909                         "username" : self.username,
910                         "password" : self.password,
911                 }
912
913                 # Send update to the server.
914                 response = self.send_request(self.url, data=data)
915
916                 # Get the full response message.
917                 output = response.read()
918
919                 # Handle success messages.
920                 if output.startswith("200"):
921                         return
922
923                 # Handle error codes.
924                 if output.startswith("400"):
925                         raise DDNSRequestError(_("Malformed request has been sent."))
926                 elif output.startswith("401"):
927                         raise DDNSAuthenticationError
928                 elif output.startswith("402"):
929                         raise DDNSRequestError(_("Too frequent update requests have been sent."))
930                 elif output.startswith("403"):
931                         raise DDNSInternalServerError
932
933                 # If we got here, some other update error happened.
934                 raise DDNSUpdateError(_("Server response: %s") % output) 
935
936
937 class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
938         handle    = "enom.com"
939         name      = "eNom Inc."
940         website   = "http://www.enom.com/"
941         protocols = ("ipv4",)
942
943         # There are very detailed information about how to send an update request and
944         # the respone codes.
945         # http://www.enom.com/APICommandCatalog/
946
947         url = "https://dynamic.name-services.com/interface.asp"
948         can_remove_records = False
949
950         def update_protocol(self, proto):
951                 data = {
952                         "command"        : "setdnshost",
953                         "responsetype"   : "xml",
954                         "address"        : self.get_address(proto),
955                         "domainpassword" : self.password,
956                         "zone"           : self.hostname
957                 }
958
959                 # Send update to the server.
960                 response = self.send_request(self.url, data=data)
961
962                 # Get the full response message.
963                 output = response.read()
964
965                 # Handle success messages.
966                 if self.get_xml_tag_value(output, "ErrCount") == "0":
967                         return
968
969                 # Handle error codes.
970                 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
971
972                 if errorcode == "304155":
973                         raise DDNSAuthenticationError
974                 elif errorcode == "304153":
975                         raise DDNSRequestError(_("Domain not found."))
976
977                 # If we got here, some other update error happened.
978                 raise DDNSUpdateError
979
980
981 class DDNSProviderEntryDNS(DDNSProvider):
982         handle    = "entrydns.net"
983         name      = "EntryDNS"
984         website   = "http://entrydns.net/"
985         protocols = ("ipv4",)
986
987         # Some very tiny details about their so called "Simple API" can be found
988         # here: https://entrydns.net/help
989         url = "https://entrydns.net/records/modify"
990         can_remove_records = False
991
992         def update_protocol(self, proto):
993                 data = {
994                         "ip" : self.get_address(proto),
995                 }
996
997                 # Add auth token to the update url.
998                 url = "%s/%s" % (self.url, self.token)
999
1000                 # Send update to the server.
1001                 try:
1002                         response = self.send_request(url, data=data)
1003
1004                 # Handle error codes
1005                 except urllib2.HTTPError, e:
1006                         if e.code == 404:
1007                                 raise DDNSAuthenticationError
1008
1009                         elif e.code == 422:
1010                                 raise DDNSRequestError(_("An invalid IP address was submitted"))
1011
1012                         raise
1013
1014                 # Handle success messages.
1015                 if response.code == 200:
1016                         return
1017
1018                 # If we got here, some other update error happened.
1019                 raise DDNSUpdateError
1020
1021
1022 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
1023         handle    = "freedns.afraid.org"
1024         name      = "freedns.afraid.org"
1025         website   = "http://freedns.afraid.org/"
1026
1027         # No information about the request or response could be found on the vendor
1028         # page. All used values have been collected by testing.
1029         url = "https://freedns.afraid.org/dynamic/update.php"
1030         can_remove_records = False
1031
1032         def update_protocol(self, proto):
1033                 data = {
1034                         "address" : self.get_address(proto),
1035                 }
1036
1037                 # Add auth token to the update url.
1038                 url = "%s?%s" % (self.url, self.token)
1039
1040                 # Send update to the server.
1041                 response = self.send_request(url, data=data)
1042
1043                 # Get the full response message.
1044                 output = response.read()
1045
1046                 # Handle success messages.
1047                 if output.startswith("Updated") or "has not changed" in output:
1048                         return
1049
1050                 # Handle error codes.
1051                 if output == "ERROR: Unable to locate this record":
1052                         raise DDNSAuthenticationError
1053                 elif "is an invalid IP address" in output:
1054                         raise DDNSRequestError(_("Invalid IP address has been sent."))
1055
1056                 # If we got here, some other update error happened.
1057                 raise DDNSUpdateError
1058
1059
1060 class DDNSProviderJoker(DDNSProtocolDynDNS2, DDNSProvider):
1061                 handle  = "joker.com"
1062                 name    = "Joker.com Dynamic DNS"
1063                 website = "https://joker.com/"
1064                 protocols = ("ipv4",)
1065
1066                 # Information about the request can be found here:
1067                 # https://joker.com/faq/content/11/427/en/what-is-dynamic-dns-dyndns.html
1068                 # Using DynDNS V2 protocol over HTTPS here
1069
1070                 url = "https://svc.joker.com/nic/update"
1071
1072
1073 class DDNSProviderGoogle(DDNSProtocolDynDNS2, DDNSProvider):
1074         handle    = "domains.google.com"
1075         name      = "Google Domains"
1076         website   = "https://domains.google.com/"
1077         protocols = ("ipv4",)
1078
1079         # Information about the format of the HTTP request is to be found
1080         # here: https://support.google.com/domains/answer/6147083?hl=en
1081
1082         url = "https://domains.google.com/nic/update"
1083
1084
1085 class DDNSProviderLightningWireLabs(DDNSProvider):
1086         handle    = "dns.lightningwirelabs.com"
1087         name      = "Lightning Wire Labs DNS Service"
1088         website   = "http://dns.lightningwirelabs.com/"
1089
1090         # Information about the format of the HTTPS request is to be found
1091         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
1092
1093         url = "https://dns.lightningwirelabs.com/update"
1094
1095         def update(self):
1096                 data =  {
1097                         "hostname" : self.hostname,
1098                         "address6" : self.get_address("ipv6", "-"),
1099                         "address4" : self.get_address("ipv4", "-"),
1100                 }
1101
1102                 # Check if a token has been set.
1103                 if self.token:
1104                         data["token"] = self.token
1105
1106                 # Check for username and password.
1107                 elif self.username and self.password:
1108                         data.update({
1109                                 "username" : self.username,
1110                                 "password" : self.password,
1111                         })
1112
1113                 # Raise an error if no auth details are given.
1114                 else:
1115                         raise DDNSConfigurationError
1116
1117                 # Send update to the server.
1118                 response = self.send_request(self.url, data=data)
1119
1120                 # Handle success messages.
1121                 if response.code == 200:
1122                         return
1123
1124                 # If we got here, some other update error happened.
1125                 raise DDNSUpdateError
1126
1127
1128 class DDNSProviderLoopia(DDNSProtocolDynDNS2, DDNSProvider):
1129         handle    = "loopia.se"
1130         name      = "Loopia AB"
1131         website   = "https://www.loopia.com"
1132         protocols = ("ipv4",)
1133
1134         # Information about the format of the HTTP request is to be found
1135         # here: https://support.loopia.com/wiki/About_the_DynDNS_support
1136
1137         url = "https://dns.loopia.se/XDynDNSServer/XDynDNS.php"
1138
1139
1140 class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
1141         handle    = "myonlineportal.net"
1142         name      = "myonlineportal.net"
1143         website   = "https:/myonlineportal.net/"
1144
1145         # Information about the request and response can be obtained here:
1146         # https://myonlineportal.net/howto_dyndns
1147
1148         url = "https://myonlineportal.net/updateddns"
1149
1150         def prepare_request_data(self, proto):
1151                 data = {
1152                         "hostname" : self.hostname,
1153                         "ip"     : self.get_address(proto),
1154                 }
1155
1156                 return data
1157
1158
1159 class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
1160         handle    = "namecheap.com"
1161         name      = "Namecheap"
1162         website   = "http://namecheap.com"
1163         protocols = ("ipv4",)
1164
1165         # Information about the format of the HTTP request is to be found
1166         # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
1167         # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
1168
1169         url = "https://dynamicdns.park-your-domain.com/update"
1170         can_remove_records = False
1171
1172         def update_protocol(self, proto):
1173                 # Namecheap requires the hostname splitted into a host and domain part.
1174                 host, domain = self.hostname.split(".", 1)
1175
1176                 data = {
1177                         "ip"       : self.get_address(proto),
1178                         "password" : self.password,
1179                         "host"     : host,
1180                         "domain"   : domain
1181                 }
1182
1183                 # Send update to the server.
1184                 response = self.send_request(self.url, data=data)
1185
1186                 # Get the full response message.
1187                 output = response.read()
1188
1189                 # Handle success messages.
1190                 if self.get_xml_tag_value(output, "IP") == address:
1191                         return
1192
1193                 # Handle error codes.
1194                 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
1195
1196                 if errorcode == "304156":
1197                         raise DDNSAuthenticationError
1198                 elif errorcode == "316153":
1199                         raise DDNSRequestError(_("Domain not found."))
1200                 elif errorcode == "316154":
1201                         raise DDNSRequestError(_("Domain not active."))
1202                 elif errorcode in ("380098", "380099"):
1203                         raise DDNSInternalServerError
1204
1205                 # If we got here, some other update error happened.
1206                 raise DDNSUpdateError
1207
1208
1209 class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
1210         handle    = "no-ip.com"
1211         name      = "No-IP"
1212         website   = "http://www.no-ip.com/"
1213         protocols = ("ipv4",)
1214
1215         # Information about the format of the HTTP request is to be found
1216         # here: http://www.no-ip.com/integrate/request and
1217         # here: http://www.no-ip.com/integrate/response
1218
1219         url = "http://dynupdate.no-ip.com/nic/update"
1220
1221         def prepare_request_data(self, proto):
1222                 assert proto == "ipv4"
1223
1224                 data = {
1225                         "hostname" : self.hostname,
1226                         "address"  : self.get_address(proto),
1227                 }
1228
1229                 return data
1230
1231
1232 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
1233         handle    = "nsupdate.info"
1234         name      = "nsupdate.info"
1235         website   = "http://nsupdate.info/"
1236         protocols = ("ipv6", "ipv4",)
1237
1238         # Information about the format of the HTTP request can be found
1239         # after login on the provider user interface and here:
1240         # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1241
1242         url = "https://nsupdate.info/nic/update"
1243
1244         # TODO nsupdate.info can actually do this, but the functionality
1245         # has not been implemented here, yet.
1246         can_remove_records = False
1247
1248         # After a failed update, there will be no retries
1249         # https://bugzilla.ipfire.org/show_bug.cgi?id=10603
1250         holdoff_failure_days = None
1251
1252         # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1253         # and for the password a so called secret.
1254         @property
1255         def username(self):
1256                 return self.get("hostname")
1257
1258         @property
1259         def password(self):
1260                 return self.token or self.get("secret")
1261
1262         def prepare_request_data(self, proto):
1263                 data = {
1264                         "myip" : self.get_address(proto),
1265                 }
1266
1267                 return data
1268
1269
1270 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
1271         handle    = "opendns.com"
1272         name      = "OpenDNS"
1273         website   = "http://www.opendns.com"
1274
1275         # Detailed information about the update request and possible
1276         # response codes can be obtained from here:
1277         # https://support.opendns.com/entries/23891440
1278
1279         url = "https://updates.opendns.com/nic/update"
1280
1281         def prepare_request_data(self, proto):
1282                 data = {
1283                         "hostname" : self.hostname,
1284                         "myip"     : self.get_address(proto),
1285                 }
1286
1287                 return data
1288
1289
1290 class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1291         handle    = "ovh.com"
1292         name      = "OVH"
1293         website   = "http://www.ovh.com/"
1294         protocols = ("ipv4",)
1295
1296         # OVH only provides very limited information about how to
1297         # update a DynDNS host. They only provide the update url
1298         # on the their german subpage.
1299         #
1300         # http://hilfe.ovh.de/DomainDynHost
1301
1302         url = "https://www.ovh.com/nic/update"
1303
1304         def prepare_request_data(self, proto):
1305                 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1306                 data.update({
1307                         "system" : "dyndns",
1308                 })
1309
1310                 return data
1311
1312
1313 class DDNSProviderRegfish(DDNSProvider):
1314         handle  = "regfish.com"
1315         name    = "Regfish GmbH"
1316         website = "http://www.regfish.com/"
1317
1318         # A full documentation to the providers api can be found here
1319         # but is only available in german.
1320         # https://www.regfish.de/domains/dyndns/dokumentation
1321
1322         url = "https://dyndns.regfish.de/"
1323         can_remove_records = False
1324
1325         def update(self):
1326                 data = {
1327                         "fqdn" : self.hostname,
1328                 }
1329
1330                 # Check if we update an IPv6 address.
1331                 address6 = self.get_address("ipv6")
1332                 if address6:
1333                         data["ipv6"] = address6
1334
1335                 # Check if we update an IPv4 address.
1336                 address4 = self.get_address("ipv4")
1337                 if address4:
1338                         data["ipv4"] = address4
1339
1340                 # Raise an error if none address is given.
1341                 if not data.has_key("ipv6") and not data.has_key("ipv4"):
1342                         raise DDNSConfigurationError
1343
1344                 # Check if a token has been set.
1345                 if self.token:
1346                         data["token"] = self.token
1347
1348                 # Raise an error if no token and no useranem and password
1349                 # are given.
1350                 elif not self.username and not self.password:
1351                         raise DDNSConfigurationError(_("No Auth details specified."))
1352
1353                 # HTTP Basic Auth is only allowed if no token is used.
1354                 if self.token:
1355                         # Send update to the server.
1356                         response = self.send_request(self.url, data=data)
1357                 else:
1358                         # Send update to the server.
1359                         response = self.send_request(self.url, username=self.username, password=self.password,
1360                                 data=data)
1361
1362                 # Get the full response message.
1363                 output = response.read()
1364
1365                 # Handle success messages.
1366                 if "100" in output or "101" in output:
1367                         return
1368
1369                 # Handle error codes.
1370                 if "401" or "402" in output:
1371                         raise DDNSAuthenticationError
1372                 elif "408" in output:
1373                         raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1374                 elif "409" in output:
1375                         raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1376                 elif "412" in output:
1377                         raise DDNSRequestError(_("No valid FQDN was given."))
1378                 elif "414" in output:
1379                         raise DDNSInternalServerError
1380
1381                 # If we got here, some other update error happened.
1382                 raise DDNSUpdateError
1383
1384
1385 class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
1386         handle    = "selfhost.de"
1387         name      = "Selfhost.de"
1388         website   = "http://www.selfhost.de/"
1389         protocols = ("ipv4",)
1390
1391         url = "https://carol.selfhost.de/nic/update"
1392
1393         def prepare_request_data(self, proto):
1394                 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1395                 data.update({
1396                         "hostname" : "1",
1397                 })
1398
1399                 return data
1400
1401
1402 class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1403         handle    = "spdns.org"
1404         name      = "SPDNS"
1405         website   = "http://spdns.org/"
1406
1407         # Detailed information about request and response codes are provided
1408         # by the vendor. They are using almost the same mechanism and status
1409         # codes as dyndns.org so we can inherit all those stuff.
1410         #
1411         # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1412         # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1413
1414         url = "https://update.spdns.de/nic/update"
1415
1416         @property
1417         def username(self):
1418                 return self.get("username") or self.hostname
1419
1420         @property
1421         def password(self):
1422                 return self.get("password") or self.token
1423
1424
1425 class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1426         handle    = "strato.com"
1427         name      = "Strato AG"
1428         website   = "http:/www.strato.com/"
1429         protocols = ("ipv4",)
1430
1431         # Information about the request and response can be obtained here:
1432         # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1433
1434         url = "https://dyndns.strato.com/nic/update"
1435
1436         def prepare_request_data(self, proto):
1437                 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1438                 data.update({
1439                         "mx" : "NOCHG",
1440                         "backupmx" : "NOCHG"
1441                 })
1442
1443                 return data
1444
1445
1446 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1447         handle    = "twodns.de"
1448         name      = "TwoDNS"
1449         website   = "http://www.twodns.de"
1450         protocols = ("ipv4",)
1451
1452         # Detailed information about the request can be found here
1453         # http://twodns.de/en/faqs
1454         # http://twodns.de/en/api
1455
1456         url = "https://update.twodns.de/update"
1457
1458         def prepare_request_data(self, proto):
1459                 assert proto == "ipv4"
1460
1461                 data = {
1462                         "ip"       : self.get_address(proto),
1463                         "hostname" : self.hostname
1464                 }
1465
1466                 return data
1467
1468
1469 class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1470         handle    = "udmedia.de"
1471         name      = "Udmedia GmbH"
1472         website   = "http://www.udmedia.de"
1473         protocols = ("ipv4",)
1474
1475         # Information about the request can be found here
1476         # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1477
1478         url = "https://www.udmedia.de/nic/update"
1479
1480
1481 class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
1482         handle    = "variomedia.de"
1483         name      = "Variomedia"
1484         website   = "http://www.variomedia.de/"
1485         protocols = ("ipv6", "ipv4",)
1486
1487         # Detailed information about the request can be found here
1488         # https://dyndns.variomedia.de/
1489
1490         url = "https://dyndns.variomedia.de/nic/update"
1491
1492         def prepare_request_data(self, proto):
1493                 data = {
1494                         "hostname" : self.hostname,
1495                         "myip"     : self.get_address(proto),
1496                 }
1497
1498                 return data
1499
1500
1501 class DDNSProviderXLhost(DDNSProtocolDynDNS2, DDNSProvider):
1502         handle    = "xlhost.de"
1503         name      = "XLhost"
1504         website   = "http://xlhost.de/"
1505         protocols = ("ipv4",)
1506
1507         # Information about the format of the HTTP request is to be found
1508         # here: https://xlhost.de/faq/index_html?topicId=CQA2ELIPO4SQ
1509
1510         url = "https://nsupdate.xlhost.de/"
1511
1512
1513 class DDNSProviderZoneedit(DDNSProvider):
1514         handle    = "zoneedit.com"
1515         name      = "Zoneedit"
1516         website   = "http://www.zoneedit.com"
1517         protocols = ("ipv4",)
1518
1519         # Detailed information about the request and the response codes can be
1520         # obtained here:
1521         # http://www.zoneedit.com/doc/api/other.html
1522         # http://www.zoneedit.com/faq.html
1523
1524         url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1525
1526         def update_protocol(self, proto):
1527                 data = {
1528                         "dnsto" : self.get_address(proto),
1529                         "host"  : self.hostname
1530                 }
1531
1532                 # Send update to the server.
1533                 response = self.send_request(self.url, username=self.username, password=self.password,
1534                         data=data)
1535
1536                 # Get the full response message.
1537                 output = response.read()
1538
1539                 # Handle success messages.
1540                 if output.startswith("<SUCCESS"):
1541                         return
1542
1543                 # Handle error codes.
1544                 if output.startswith("invalid login"):
1545                         raise DDNSAuthenticationError
1546                 elif output.startswith("<ERROR CODE=\"704\""):
1547                         raise DDNSRequestError(_("No valid FQDN was given.")) 
1548                 elif output.startswith("<ERROR CODE=\"702\""):
1549                         raise DDNSInternalServerError
1550
1551                 # If we got here, some other update error happened.
1552                 raise DDNSUpdateError
1553
1554
1555 class DDNSProviderDNSmadeEasy(DDNSProvider):
1556         handle    = "dnsmadeeasy.com"
1557         name      = "DNSmadeEasy.com"
1558         website   = "http://www.dnsmadeeasy.com/"
1559         protocols = ("ipv4",)
1560
1561         # DNS Made Easy Nameserver Provider also offering Dynamic DNS
1562         # Documentation can be found here:
1563         # http://www.dnsmadeeasy.com/dynamic-dns/
1564
1565         url = "https://cp.dnsmadeeasy.com/servlet/updateip?"
1566         can_remove_records = False
1567
1568         def update_protocol(self, proto):
1569                 data = {
1570                         "ip" : self.get_address(proto),
1571                         "id" : self.hostname,
1572                         "username" : self.username,
1573                         "password" : self.password,
1574                 }
1575
1576                 # Send update to the server.
1577                 response = self.send_request(self.url, data=data)
1578
1579                 # Get the full response message.
1580                 output = response.read()
1581
1582                 # Handle success messages.
1583                 if output.startswith("success") or output.startswith("error-record-ip-same"):
1584                         return
1585
1586                 # Handle error codes.
1587                 if output.startswith("error-auth-suspend"):
1588                         raise DDNSRequestError(_("Account has been suspended."))
1589
1590                 elif output.startswith("error-auth-voided"):
1591                         raise DDNSRequestError(_("Account has been revoked."))
1592
1593                 elif output.startswith("error-record-invalid"):
1594                         raise DDNSRequestError(_("Specified host does not exist."))
1595
1596                 elif output.startswith("error-auth"):
1597                         raise DDNSAuthenticationError
1598
1599                 # If we got here, some other update error happened.
1600                 raise DDNSUpdateError(_("Server response: %s") % output)
1601
1602
1603 class DDNSProviderZZZZ(DDNSProvider):
1604         handle    = "zzzz.io"
1605         name      = "zzzz"
1606         website   = "https://zzzz.io"
1607         protocols = ("ipv6", "ipv4",)
1608
1609         # Detailed information about the update request can be found here:
1610         # https://zzzz.io/faq/
1611
1612         # Details about the possible response codes have been provided in the bugtracker:
1613         # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1614
1615         url = "https://zzzz.io/api/v1/update"
1616         can_remove_records = False
1617
1618         def update_protocol(self, proto):
1619                 data = {
1620                         "ip"    : self.get_address(proto),
1621                         "token" : self.token,
1622                 }
1623
1624                 if proto == "ipv6":
1625                         data["type"] = "aaaa"
1626
1627                 # zzzz uses the host from the full hostname as part
1628                 # of the update url.
1629                 host, domain = self.hostname.split(".", 1)
1630
1631                 # Add host value to the update url.
1632                 url = "%s/%s" % (self.url, host)
1633
1634                 # Send update to the server.
1635                 try:
1636                         response = self.send_request(url, data=data)
1637
1638                 # Handle error codes.
1639                 except DDNSNotFound:
1640                         raise DDNSRequestError(_("Invalid hostname specified"))
1641
1642                 # Handle success messages.
1643                 if response.code == 200:
1644                         return
1645
1646                 # If we got here, some other update error happened.
1647                 raise DDNSUpdateError