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