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