Add runtime check for providers if all dependencies are met
[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         # True if the provider is able to remove records, too.
67         # Required to remove AAAA records if IPv6 is absent again.
68         can_remove_records = True
69
70         # Automatically register all providers.
71         class __metaclass__(type):
72                 def __init__(provider, name, bases, dict):
73                         type.__init__(provider, name, bases, dict)
74
75                         # The main class from which is inherited is not registered
76                         # as a provider.
77                         if name == "DDNSProvider":
78                                 return
79
80                         if not all((provider.handle, provider.name, provider.website)):
81                                 raise DDNSError(_("Provider is not properly configured"))
82
83                         assert not _providers.has_key(provider.handle), \
84                                 "Provider '%s' has already been registered" % provider.handle
85
86                         _providers[provider.handle] = provider
87
88         @staticmethod
89         def supported():
90                 """
91                         Should be overwritten to check if the system the code is running
92                         on has all the required tools to support this provider.
93                 """
94                 return True
95
96         def __init__(self, core, **settings):
97                 self.core = core
98
99                 # Copy a set of default settings and
100                 # update them by those from the configuration file.
101                 self.settings = self.DEFAULT_SETTINGS.copy()
102                 self.settings.update(settings)
103
104         def __repr__(self):
105                 return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
106
107         def __cmp__(self, other):
108                 return cmp(self.hostname, other.hostname)
109
110         @property
111         def db(self):
112                 return self.core.db
113
114         def get(self, key, default=None):
115                 """
116                         Get a setting from the settings dictionary.
117                 """
118                 return self.settings.get(key, default)
119
120         @property
121         def hostname(self):
122                 """
123                         Fast access to the hostname.
124                 """
125                 return self.get("hostname")
126
127         @property
128         def username(self):
129                 """
130                         Fast access to the username.
131                 """
132                 return self.get("username")
133
134         @property
135         def password(self):
136                 """
137                         Fast access to the password.
138                 """
139                 return self.get("password")
140
141         @property
142         def token(self):
143                 """
144                         Fast access to the token.
145                 """
146                 return self.get("token")
147
148         def __call__(self, force=False):
149                 if force:
150                         logger.debug(_("Updating %s forced") % self.hostname)
151
152                 # Do nothing if no update is required
153                 elif not self.requires_update:
154                         return
155
156                 # Execute the update.
157                 try:
158                         self.update()
159
160                 # In case of any errors, log the failed request and
161                 # raise the exception.
162                 except DDNSError as e:
163                         self.core.db.log_failure(self.hostname, e)
164                         raise
165
166                 logger.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \
167                         { "hostname" : self.hostname, "provider" : self.name })
168                 self.core.db.log_success(self.hostname)
169
170         def update(self):
171                 for protocol in self.protocols:
172                         if self.have_address(protocol):
173                                 self.update_protocol(protocol)
174                         elif self.can_remove_records:
175                                 self.remove_protocol(protocol)
176
177         def update_protocol(self, proto):
178                 raise NotImplementedError
179
180         def remove_protocol(self, proto):
181                 if not self.can_remove_records:
182                         raise RuntimeError, "can_remove_records is enabled, but remove_protocol() not implemented"
183
184                 raise NotImplementedError
185
186         @property
187         def requires_update(self):
188                 # If the IP addresses have changed, an update is required
189                 if self.ip_address_changed(self.protocols):
190                         logger.debug(_("An update for %(hostname)s (%(provider)s)"
191                                 " is performed because of an IP address change") % \
192                                 { "hostname" : self.hostname, "provider" : self.name })
193
194                         return True
195
196                 # If the holdoff time has expired, an update is required, too
197                 if self.holdoff_time_expired():
198                         logger.debug(_("An update for %(hostname)s (%(provider)s)"
199                                 " is performed because the holdoff time has expired") % \
200                                 { "hostname" : self.hostname, "provider" : self.name })
201
202                         return True
203
204                 # Otherwise, we don't need to perform an update
205                 logger.debug(_("No update required for %(hostname)s (%(provider)s)") % \
206                         { "hostname" : self.hostname, "provider" : self.name })
207
208                 return False
209
210         def ip_address_changed(self, protos):
211                 """
212                         Returns True if this host is already up to date
213                         and does not need to change the IP address on the
214                         name server.
215                 """
216                 for proto in protos:
217                         addresses = self.core.system.resolve(self.hostname, proto)
218                         current_address = self.get_address(proto)
219
220                         # Handle if the system has not got any IP address from a protocol
221                         # (i.e. had full dual-stack connectivity which it has not any more)
222                         if current_address is None:
223                                 # If addresses still exists in the DNS system and if this provider
224                                 # is able to remove records, we will do that.
225                                 if addresses and self.can_remove_records:
226                                         return True
227
228                                 # Otherwise, we cannot go on...
229                                 continue
230
231                         if not current_address in addresses:
232                                 return True
233
234                 return False
235
236         def holdoff_time_expired(self):
237                 """
238                         Returns true if the holdoff time has expired
239                         and the host requires an update
240                 """
241                 # If no holdoff days is defined, we cannot go on
242                 if not self.holdoff_days:
243                         return False
244
245                 # Get the timestamp of the last successfull update
246                 last_update = self.db.last_update(self.hostname)
247
248                 # If no timestamp has been recorded, no update has been
249                 # performed. An update should be performed now.
250                 if not last_update:
251                         return True
252
253                 # Determine when the holdoff time ends
254                 holdoff_end = last_update + datetime.timedelta(days=self.holdoff_days)
255
256                 now = datetime.datetime.utcnow()
257
258                 if now >= holdoff_end:
259                         logger.debug("The holdoff time has expired for %s" % self.hostname)
260                         return True
261                 else:
262                         logger.debug("Updates for %s are held off until %s" % \
263                                 (self.hostname, holdoff_end))
264                         return False
265
266         def send_request(self, *args, **kwargs):
267                 """
268                         Proxy connection to the send request
269                         method.
270                 """
271                 return self.core.system.send_request(*args, **kwargs)
272
273         def get_address(self, proto, default=None):
274                 """
275                         Proxy method to get the current IP address.
276                 """
277                 return self.core.system.get_address(proto) or default
278
279         def have_address(self, proto):
280                 """
281                         Returns True if an IP address for the given protocol
282                         is known and usable.
283                 """
284                 address = self.get_address(proto)
285
286                 if address:
287                         return True
288
289                 return False
290
291
292 class DDNSProtocolDynDNS2(object):
293         """
294                 This is an abstract class that implements the DynDNS updater
295                 protocol version 2. As this is a popular way to update dynamic
296                 DNS records, this class is supposed make the provider classes
297                 shorter and simpler.
298         """
299
300         # Information about the format of the request is to be found
301         # http://dyn.com/support/developers/api/perform-update/
302         # http://dyn.com/support/developers/api/return-codes/
303
304         # The DynDNS protocol version 2 does not allow to remove records
305         can_remove_records = False
306
307         def prepare_request_data(self, proto):
308                 data = {
309                         "hostname" : self.hostname,
310                         "myip"     : self.get_address(proto),
311                 }
312
313                 return data
314
315         def update_protocol(self, proto):
316                 data = self.prepare_request_data(proto)
317
318                 return self.send_request(data)
319
320         def send_request(self, data):
321                 # Send update to the server.
322                 response = DDNSProvider.send_request(self, self.url, data=data,
323                         username=self.username, password=self.password)
324
325                 # Get the full response message.
326                 output = response.read()
327
328                 # Handle success messages.
329                 if output.startswith("good") or output.startswith("nochg"):
330                         return
331
332                 # Handle error codes.
333                 if output == "badauth":
334                         raise DDNSAuthenticationError
335                 elif output == "abuse":
336                         raise DDNSAbuseError
337                 elif output == "notfqdn":
338                         raise DDNSRequestError(_("No valid FQDN was given."))
339                 elif output == "nohost":
340                         raise DDNSRequestError(_("Specified host does not exist."))
341                 elif output == "911":
342                         raise DDNSInternalServerError
343                 elif output == "dnserr":
344                         raise DDNSInternalServerError(_("DNS error encountered."))
345                 elif output == "badagent":
346                         raise DDNSBlockedError
347
348                 # If we got here, some other update error happened.
349                 raise DDNSUpdateError(_("Server response: %s") % output)
350
351
352 class DDNSResponseParserXML(object):
353         """
354                 This class provides a parser for XML responses which
355                 will be sent by various providers. This class uses the python
356                 shipped XML minidom module to walk through the XML tree and return
357                 a requested element.
358         """
359
360         def get_xml_tag_value(self, document, content):
361                 # Send input to the parser.
362                 xmldoc = xml.dom.minidom.parseString(document)
363
364                 # Get XML elements by the given content.
365                 element = xmldoc.getElementsByTagName(content)
366
367                 # If no element has been found, we directly can return None.
368                 if not element:
369                         return None
370
371                 # Only get the first child from an element, even there are more than one.
372                 firstchild = element[0].firstChild
373
374                 # Get the value of the child.
375                 value = firstchild.nodeValue
376
377                 # Return the value.
378                 return value
379
380
381 class DDNSProviderAllInkl(DDNSProvider):
382         handle    = "all-inkl.com"
383         name      = "All-inkl.com"
384         website   = "http://all-inkl.com/"
385         protocols = ("ipv4",)
386
387         # There are only information provided by the vendor how to
388         # perform an update on a FRITZ Box. Grab requried informations
389         # from the net.
390         # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
391
392         url = "http://dyndns.kasserver.com"
393         can_remove_records = False
394
395         def update(self):
396                 # There is no additional data required so we directly can
397                 # send our request.
398                 response = self.send_request(self.url, username=self.username, password=self.password)
399
400                 # Get the full response message.
401                 output = response.read()
402
403                 # Handle success messages.
404                 if output.startswith("good") or output.startswith("nochg"):
405                         return
406
407                 # If we got here, some other update error happened.
408                 raise DDNSUpdateError
409
410
411 class DDNSProviderBindNsupdate(DDNSProvider):
412         handle  = "nsupdate"
413         name    = "BIND nsupdate utility"
414         website = "http://en.wikipedia.org/wiki/Nsupdate"
415
416         DEFAULT_TTL = 60
417
418         @staticmethod
419         def supported():
420                 # Search if the nsupdate utility is available
421                 paths = os.environ.get("PATH")
422
423                 for path in paths.split(":"):
424                         executable = os.path.join(path, "nsupdate")
425
426                         if os.path.exists(executable):
427                                 return True
428
429                 return False
430
431         def update(self):
432                 scriptlet = self.__make_scriptlet()
433
434                 # -v enables TCP hence we transfer keys and other data that may
435                 # exceed the size of one packet.
436                 # -t sets the timeout
437                 command = ["nsupdate", "-v", "-t", "60"]
438
439                 p = subprocess.Popen(command, shell=True,
440                         stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
441                 )
442                 stdout, stderr = p.communicate(scriptlet)
443
444                 if p.returncode == 0:
445                         return
446
447                 raise DDNSError("nsupdate terminated with error code: %s\n  %s" % (p.returncode, stderr))
448
449         def __make_scriptlet(self):
450                 scriptlet = []
451
452                 # Set a different server the update is sent to.
453                 server = self.get("server", None)
454                 if server:
455                         scriptlet.append("server %s" % server)
456
457                 # Set the DNS zone the host should be added to.
458                 zone = self.get("zone", None)
459                 if zone:
460                         scriptlet.append("zone %s" % zone)
461
462                 key = self.get("key", None)
463                 if key:
464                         secret = self.get("secret")
465
466                         scriptlet.append("key %s %s" % (key, secret))
467
468                 ttl = self.get("ttl", self.DEFAULT_TTL)
469
470                 # Perform an update for each supported protocol.
471                 for rrtype, proto in (("AAAA", "ipv6"), ("A", "ipv4")):
472                         address = self.get_address(proto)
473                         if not address:
474                                 continue
475
476                         scriptlet.append("update delete %s. %s" % (self.hostname, rrtype))
477                         scriptlet.append("update add %s. %s %s %s" % \
478                                 (self.hostname, ttl, rrtype, address))
479
480                 # Send the actions to the server.
481                 scriptlet.append("send")
482                 scriptlet.append("quit")
483
484                 logger.debug(_("Scriptlet:"))
485                 for line in scriptlet:
486                         # Masquerade the line with the secret key.
487                         if line.startswith("key"):
488                                 line = "key **** ****"
489
490                         logger.debug("  %s" % line)
491
492                 return "\n".join(scriptlet)
493
494
495 class DDNSProviderDHS(DDNSProvider):
496         handle    = "dhs.org"
497         name      = "DHS International"
498         website   = "http://dhs.org/"
499         protocols = ("ipv4",)
500
501         # No information about the used update api provided on webpage,
502         # grabed from source code of ez-ipudate.
503
504         url = "http://members.dhs.org/nic/hosts"
505         can_remove_records = False
506
507         def update_protocol(self, proto):
508                 data = {
509                         "domain"       : self.hostname,
510                         "ip"           : self.get_address(proto),
511                         "hostcmd"      : "edit",
512                         "hostcmdstage" : "2",
513                         "type"         : "4",
514                 }
515
516                 # Send update to the server.
517                 response = self.send_request(self.url, username=self.username, password=self.password,
518                         data=data)
519
520                 # Handle success messages.
521                 if response.code == 200:
522                         return
523
524                 # If we got here, some other update error happened.
525                 raise DDNSUpdateError
526
527
528 class DDNSProviderDNSpark(DDNSProvider):
529         handle    = "dnspark.com"
530         name      = "DNS Park"
531         website   = "http://dnspark.com/"
532         protocols = ("ipv4",)
533
534         # Informations to the used api can be found here:
535         # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
536
537         url = "https://control.dnspark.com/api/dynamic/update.php"
538         can_remove_records = False
539
540         def update_protocol(self, proto):
541                 data = {
542                         "domain" : self.hostname,
543                         "ip"     : self.get_address(proto),
544                 }
545
546                 # Send update to the server.
547                 response = self.send_request(self.url, username=self.username, password=self.password,
548                         data=data)
549
550                 # Get the full response message.
551                 output = response.read()
552
553                 # Handle success messages.
554                 if output.startswith("ok") or output.startswith("nochange"):
555                         return
556
557                 # Handle error codes.
558                 if output == "unauth":
559                         raise DDNSAuthenticationError
560                 elif output == "abuse":
561                         raise DDNSAbuseError
562                 elif output == "blocked":
563                         raise DDNSBlockedError
564                 elif output == "nofqdn":
565                         raise DDNSRequestError(_("No valid FQDN was given."))
566                 elif output == "nohost":
567                         raise DDNSRequestError(_("Invalid hostname specified."))
568                 elif output == "notdyn":
569                         raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
570                 elif output == "invalid":
571                         raise DDNSRequestError(_("Invalid IP address has been sent."))
572
573                 # If we got here, some other update error happened.
574                 raise DDNSUpdateError
575
576
577 class DDNSProviderDtDNS(DDNSProvider):
578         handle    = "dtdns.com"
579         name      = "DtDNS"
580         website   = "http://dtdns.com/"
581         protocols = ("ipv4",)
582
583         # Information about the format of the HTTPS request is to be found
584         # http://www.dtdns.com/dtsite/updatespec
585
586         url = "https://www.dtdns.com/api/autodns.cfm"
587         can_remove_records = False
588
589         def update_protocol(self, proto):
590                 data = {
591                         "ip" : self.get_address(proto),
592                         "id" : self.hostname,
593                         "pw" : self.password
594                 }
595
596                 # Send update to the server.
597                 response = self.send_request(self.url, data=data)
598
599                 # Get the full response message.
600                 output = response.read()
601
602                 # Remove all leading and trailing whitespace.
603                 output = output.strip()
604
605                 # Handle success messages.
606                 if "now points to" in output:
607                         return
608
609                 # Handle error codes.
610                 if output == "No hostname to update was supplied.":
611                         raise DDNSRequestError(_("No hostname specified."))
612
613                 elif output == "The hostname you supplied is not valid.":
614                         raise DDNSRequestError(_("Invalid hostname specified."))
615
616                 elif output == "The password you supplied is not valid.":
617                         raise DDNSAuthenticationError
618
619                 elif output == "Administration has disabled this account.":
620                         raise DDNSRequestError(_("Account has been disabled."))
621
622                 elif output == "Illegal character in IP.":
623                         raise DDNSRequestError(_("Invalid IP address has been sent."))
624
625                 elif output == "Too many failed requests.":
626                         raise DDNSRequestError(_("Too many failed requests."))
627
628                 # If we got here, some other update error happened.
629                 raise DDNSUpdateError
630
631
632 class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
633         handle    = "dyndns.org"
634         name      = "Dyn"
635         website   = "http://dyn.com/dns/"
636         protocols = ("ipv4",)
637
638         # Information about the format of the request is to be found
639         # http://http://dyn.com/support/developers/api/perform-update/
640         # http://dyn.com/support/developers/api/return-codes/
641
642         url = "https://members.dyndns.org/nic/update"
643
644
645 class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
646         handle    = "dynu.com"
647         name      = "Dynu"
648         website   = "http://dynu.com/"
649         protocols = ("ipv6", "ipv4",)
650
651         # Detailed information about the request and response codes
652         # are available on the providers webpage.
653         # http://dynu.com/Default.aspx?page=dnsapi
654
655         url = "https://api.dynu.com/nic/update"
656
657         # DynU sends the IPv6 and IPv4 address in one request
658
659         def update(self):
660                 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
661
662                 # This one supports IPv6
663                 myipv6 = self.get_address("ipv6")
664
665                 # Add update information if we have an IPv6 address.
666                 if myipv6:
667                         data["myipv6"] = myipv6
668
669                 self._send_request(data)
670
671
672 class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
673         handle    = "easydns.com"
674         name      = "EasyDNS"
675         website   = "http://www.easydns.com/"
676         protocols = ("ipv4",)
677
678         # There is only some basic documentation provided by the vendor,
679         # also searching the web gain very poor results.
680         # http://mediawiki.easydns.com/index.php/Dynamic_DNS
681
682         url = "http://api.cp.easydns.com/dyn/tomato.php"
683
684
685 class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
686         handle    = "domopoli.de"
687         name      = "domopoli.de"
688         website   = "http://domopoli.de/"
689         protocols = ("ipv4",)
690
691         # https://www.domopoli.de/?page=howto#DynDns_start
692
693         url = "http://dyndns.domopoli.de/nic/update"
694
695
696 class DDNSProviderDynsNet(DDNSProvider):
697         handle    = "dyns.net"
698         name      = "DyNS"
699         website   = "http://www.dyns.net/"
700         protocols = ("ipv4",)
701         can_remove_records = False
702
703         # There is very detailed informatio about how to send the update request and
704         # the possible response codes. (Currently we are using the v1.1 proto)
705         # http://www.dyns.net/documentation/technical/protocol/
706
707         url = "http://www.dyns.net/postscript011.php"
708
709         def update_protocol(self, proto):
710                 data = {
711                         "ip"       : self.get_address(proto),
712                         "host"     : self.hostname,
713                         "username" : self.username,
714                         "password" : self.password,
715                 }
716
717                 # Send update to the server.
718                 response = self.send_request(self.url, data=data)
719
720                 # Get the full response message.
721                 output = response.read()
722
723                 # Handle success messages.
724                 if output.startswith("200"):
725                         return
726
727                 # Handle error codes.
728                 if output.startswith("400"):
729                         raise DDNSRequestError(_("Malformed request has been sent."))
730                 elif output.startswith("401"):
731                         raise DDNSAuthenticationError
732                 elif output.startswith("402"):
733                         raise DDNSRequestError(_("Too frequent update requests have been sent."))
734                 elif output.startswith("403"):
735                         raise DDNSInternalServerError
736
737                 # If we got here, some other update error happened.
738                 raise DDNSUpdateError(_("Server response: %s") % output) 
739
740
741 class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
742         handle    = "enom.com"
743         name      = "eNom Inc."
744         website   = "http://www.enom.com/"
745         protocols = ("ipv4",)
746
747         # There are very detailed information about how to send an update request and
748         # the respone codes.
749         # http://www.enom.com/APICommandCatalog/
750
751         url = "https://dynamic.name-services.com/interface.asp"
752         can_remove_records = False
753
754         def update_protocol(self, proto):
755                 data = {
756                         "command"        : "setdnshost",
757                         "responsetype"   : "xml",
758                         "address"        : self.get_address(proto),
759                         "domainpassword" : self.password,
760                         "zone"           : self.hostname
761                 }
762
763                 # Send update to the server.
764                 response = self.send_request(self.url, data=data)
765
766                 # Get the full response message.
767                 output = response.read()
768
769                 # Handle success messages.
770                 if self.get_xml_tag_value(output, "ErrCount") == "0":
771                         return
772
773                 # Handle error codes.
774                 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
775
776                 if errorcode == "304155":
777                         raise DDNSAuthenticationError
778                 elif errorcode == "304153":
779                         raise DDNSRequestError(_("Domain not found."))
780
781                 # If we got here, some other update error happened.
782                 raise DDNSUpdateError
783
784
785 class DDNSProviderEntryDNS(DDNSProvider):
786         handle    = "entrydns.net"
787         name      = "EntryDNS"
788         website   = "http://entrydns.net/"
789         protocols = ("ipv4",)
790
791         # Some very tiny details about their so called "Simple API" can be found
792         # here: https://entrydns.net/help
793         url = "https://entrydns.net/records/modify"
794         can_remove_records = False
795
796         def update_protocol(self, proto):
797                 data = {
798                         "ip" : self.get_address(proto),
799                 }
800
801                 # Add auth token to the update url.
802                 url = "%s/%s" % (self.url, self.token)
803
804                 # Send update to the server.
805                 try:
806                         response = self.send_request(url, data=data)
807
808                 # Handle error codes
809                 except urllib2.HTTPError, e:
810                         if e.code == 404:
811                                 raise DDNSAuthenticationError
812
813                         elif e.code == 422:
814                                 raise DDNSRequestError(_("An invalid IP address was submitted"))
815
816                         raise
817
818                 # Handle success messages.
819                 if response.code == 200:
820                         return
821
822                 # If we got here, some other update error happened.
823                 raise DDNSUpdateError
824
825
826 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
827         handle    = "freedns.afraid.org"
828         name      = "freedns.afraid.org"
829         website   = "http://freedns.afraid.org/"
830
831         # No information about the request or response could be found on the vendor
832         # page. All used values have been collected by testing.
833         url = "https://freedns.afraid.org/dynamic/update.php"
834         can_remove_records = False
835
836         def update_protocol(self, proto):
837                 data = {
838                         "address" : self.get_address(proto),
839                 }
840
841                 # Add auth token to the update url.
842                 url = "%s?%s" % (self.url, self.token)
843
844                 # Send update to the server.
845                 response = self.send_request(url, data=data)
846
847                 # Get the full response message.
848                 output = response.read()
849
850                 # Handle success messages.
851                 if output.startswith("Updated") or "has not changed" in output:
852                         return
853
854                 # Handle error codes.
855                 if output == "ERROR: Unable to locate this record":
856                         raise DDNSAuthenticationError
857                 elif "is an invalid IP address" in output:
858                         raise DDNSRequestError(_("Invalid IP address has been sent."))
859
860                 # If we got here, some other update error happened.
861                 raise DDNSUpdateError
862
863
864 class DDNSProviderLightningWireLabs(DDNSProvider):
865         handle    = "dns.lightningwirelabs.com"
866         name      = "Lightning Wire Labs DNS Service"
867         website   = "http://dns.lightningwirelabs.com/"
868
869         # Information about the format of the HTTPS request is to be found
870         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
871
872         url = "https://dns.lightningwirelabs.com/update"
873
874         def update(self):
875                 data =  {
876                         "hostname" : self.hostname,
877                         "address6" : self.get_address("ipv6", "-"),
878                         "address4" : self.get_address("ipv4", "-"),
879                 }
880
881                 # Check if a token has been set.
882                 if self.token:
883                         data["token"] = self.token
884
885                 # Check for username and password.
886                 elif self.username and self.password:
887                         data.update({
888                                 "username" : self.username,
889                                 "password" : self.password,
890                         })
891
892                 # Raise an error if no auth details are given.
893                 else:
894                         raise DDNSConfigurationError
895
896                 # Send update to the server.
897                 response = self.send_request(self.url, data=data)
898
899                 # Handle success messages.
900                 if response.code == 200:
901                         return
902
903                 # If we got here, some other update error happened.
904                 raise DDNSUpdateError
905
906
907 class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
908         handle    = "myonlineportal.net"
909         name      = "myonlineportal.net"
910         website   = "https:/myonlineportal.net/"
911
912         # Information about the request and response can be obtained here:
913         # https://myonlineportal.net/howto_dyndns
914
915         url = "https://myonlineportal.net/updateddns"
916
917         def prepare_request_data(self, proto):
918                 data = {
919                         "hostname" : self.hostname,
920                         "ip"     : self.get_address(proto),
921                 }
922
923                 return data
924
925
926 class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
927         handle    = "namecheap.com"
928         name      = "Namecheap"
929         website   = "http://namecheap.com"
930         protocols = ("ipv4",)
931
932         # Information about the format of the HTTP request is to be found
933         # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
934         # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
935
936         url = "https://dynamicdns.park-your-domain.com/update"
937         can_remove_records = False
938
939         def update_protocol(self, proto):
940                 # Namecheap requires the hostname splitted into a host and domain part.
941                 host, domain = self.hostname.split(".", 1)
942
943                 data = {
944                         "ip"       : self.get_address(proto),
945                         "password" : self.password,
946                         "host"     : host,
947                         "domain"   : domain
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, "IP") == address:
958                         return
959
960                 # Handle error codes.
961                 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
962
963                 if errorcode == "304156":
964                         raise DDNSAuthenticationError
965                 elif errorcode == "316153":
966                         raise DDNSRequestError(_("Domain not found."))
967                 elif errorcode == "316154":
968                         raise DDNSRequestError(_("Domain not active."))
969                 elif errorcode in ("380098", "380099"):
970                         raise DDNSInternalServerError
971
972                 # If we got here, some other update error happened.
973                 raise DDNSUpdateError
974
975
976 class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
977         handle    = "no-ip.com"
978         name      = "No-IP"
979         website   = "http://www.no-ip.com/"
980         protocols = ("ipv4",)
981
982         # Information about the format of the HTTP request is to be found
983         # here: http://www.no-ip.com/integrate/request and
984         # here: http://www.no-ip.com/integrate/response
985
986         url = "http://dynupdate.no-ip.com/nic/update"
987
988         def prepare_request_data(self, proto):
989                 assert proto == "ipv4"
990
991                 data = {
992                         "hostname" : self.hostname,
993                         "address"  : self.get_address(proto),
994                 }
995
996                 return data
997
998
999 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
1000         handle    = "nsupdate.info"
1001         name      = "nsupdate.info"
1002         website   = "http://nsupdate.info/"
1003         protocols = ("ipv6", "ipv4",)
1004
1005         # Information about the format of the HTTP request can be found
1006         # after login on the provider user interface and here:
1007         # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1008
1009         # TODO nsupdate.info can actually do this, but the functionality
1010         # has not been implemented here, yet.
1011         can_remove_records = False
1012
1013         # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1014         # and for the password a so called secret.
1015         @property
1016         def username(self):
1017                 return self.get("hostname")
1018
1019         @property
1020         def password(self):
1021                 return self.token or self.get("secret")
1022
1023         @property
1024         def url(self):
1025                 # The update URL is different by the used protocol.
1026                 if self.proto == "ipv4":
1027                         return "https://ipv4.nsupdate.info/nic/update"
1028                 elif self.proto == "ipv6":
1029                         return "https://ipv6.nsupdate.info/nic/update"
1030                 else:
1031                         raise DDNSUpdateError(_("Invalid protocol has been given"))
1032
1033         def prepare_request_data(self, proto):
1034                 data = {
1035                         "myip" : self.get_address(proto),
1036                 }
1037
1038                 return data
1039
1040
1041 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
1042         handle    = "opendns.com"
1043         name      = "OpenDNS"
1044         website   = "http://www.opendns.com"
1045
1046         # Detailed information about the update request and possible
1047         # response codes can be obtained from here:
1048         # https://support.opendns.com/entries/23891440
1049
1050         url = "https://updates.opendns.com/nic/update"
1051
1052         def prepare_request_data(self, proto):
1053                 data = {
1054                         "hostname" : self.hostname,
1055                         "myip"     : self.get_address(proto),
1056                 }
1057
1058                 return data
1059
1060
1061 class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1062         handle    = "ovh.com"
1063         name      = "OVH"
1064         website   = "http://www.ovh.com/"
1065         protocols = ("ipv4",)
1066
1067         # OVH only provides very limited information about how to
1068         # update a DynDNS host. They only provide the update url
1069         # on the their german subpage.
1070         #
1071         # http://hilfe.ovh.de/DomainDynHost
1072
1073         url = "https://www.ovh.com/nic/update"
1074
1075         def prepare_request_data(self, proto):
1076                 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1077                 data.update({
1078                         "system" : "dyndns",
1079                 })
1080
1081                 return data
1082
1083
1084 class DDNSProviderRegfish(DDNSProvider):
1085         handle  = "regfish.com"
1086         name    = "Regfish GmbH"
1087         website = "http://www.regfish.com/"
1088
1089         # A full documentation to the providers api can be found here
1090         # but is only available in german.
1091         # https://www.regfish.de/domains/dyndns/dokumentation
1092
1093         url = "https://dyndns.regfish.de/"
1094         can_remove_records = False
1095
1096         def update(self):
1097                 data = {
1098                         "fqdn" : self.hostname,
1099                 }
1100
1101                 # Check if we update an IPv6 address.
1102                 address6 = self.get_address("ipv6")
1103                 if address6:
1104                         data["ipv6"] = address6
1105
1106                 # Check if we update an IPv4 address.
1107                 address4 = self.get_address("ipv4")
1108                 if address4:
1109                         data["ipv4"] = address4
1110
1111                 # Raise an error if none address is given.
1112                 if not data.has_key("ipv6") and not data.has_key("ipv4"):
1113                         raise DDNSConfigurationError
1114
1115                 # Check if a token has been set.
1116                 if self.token:
1117                         data["token"] = self.token
1118
1119                 # Raise an error if no token and no useranem and password
1120                 # are given.
1121                 elif not self.username and not self.password:
1122                         raise DDNSConfigurationError(_("No Auth details specified."))
1123
1124                 # HTTP Basic Auth is only allowed if no token is used.
1125                 if self.token:
1126                         # Send update to the server.
1127                         response = self.send_request(self.url, data=data)
1128                 else:
1129                         # Send update to the server.
1130                         response = self.send_request(self.url, username=self.username, password=self.password,
1131                                 data=data)
1132
1133                 # Get the full response message.
1134                 output = response.read()
1135
1136                 # Handle success messages.
1137                 if "100" in output or "101" in output:
1138                         return
1139
1140                 # Handle error codes.
1141                 if "401" or "402" in output:
1142                         raise DDNSAuthenticationError
1143                 elif "408" in output:
1144                         raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1145                 elif "409" in output:
1146                         raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1147                 elif "412" in output:
1148                         raise DDNSRequestError(_("No valid FQDN was given."))
1149                 elif "414" in output:
1150                         raise DDNSInternalServerError
1151
1152                 # If we got here, some other update error happened.
1153                 raise DDNSUpdateError
1154
1155
1156 class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
1157         handle    = "selfhost.de"
1158         name      = "Selfhost.de"
1159         website   = "http://www.selfhost.de/"
1160         protocols = ("ipv4",)
1161
1162         url = "https://carol.selfhost.de/nic/update"
1163
1164         def prepare_request_data(self, proto):
1165                 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1166                 data.update({
1167                         "hostname" : "1",
1168                 })
1169
1170                 return data
1171
1172
1173 class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1174         handle    = "spdns.org"
1175         name      = "SPDNS"
1176         website   = "http://spdns.org/"
1177
1178         # Detailed information about request and response codes are provided
1179         # by the vendor. They are using almost the same mechanism and status
1180         # codes as dyndns.org so we can inherit all those stuff.
1181         #
1182         # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1183         # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1184
1185         url = "https://update.spdns.de/nic/update"
1186
1187         @property
1188         def username(self):
1189                 return self.get("username") or self.hostname
1190
1191         @property
1192         def password(self):
1193                 return self.get("username") or self.token
1194
1195
1196 class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1197         handle    = "strato.com"
1198         name      = "Strato AG"
1199         website   = "http:/www.strato.com/"
1200         protocols = ("ipv4",)
1201
1202         # Information about the request and response can be obtained here:
1203         # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1204
1205         url = "https://dyndns.strato.com/nic/update"
1206
1207
1208 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1209         handle    = "twodns.de"
1210         name      = "TwoDNS"
1211         website   = "http://www.twodns.de"
1212         protocols = ("ipv4",)
1213
1214         # Detailed information about the request can be found here
1215         # http://twodns.de/en/faqs
1216         # http://twodns.de/en/api
1217
1218         url = "https://update.twodns.de/update"
1219
1220         def prepare_request_data(self, proto):
1221                 assert proto == "ipv4"
1222
1223                 data = {
1224                         "ip"       : self.get_address(proto),
1225                         "hostname" : self.hostname
1226                 }
1227
1228                 return data
1229
1230
1231 class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1232         handle    = "udmedia.de"
1233         name      = "Udmedia GmbH"
1234         website   = "http://www.udmedia.de"
1235         protocols = ("ipv4",)
1236
1237         # Information about the request can be found here
1238         # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1239
1240         url = "https://www.udmedia.de/nic/update"
1241
1242
1243 class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
1244         handle    = "variomedia.de"
1245         name      = "Variomedia"
1246         website   = "http://www.variomedia.de/"
1247         protocols = ("ipv6", "ipv4",)
1248
1249         # Detailed information about the request can be found here
1250         # https://dyndns.variomedia.de/
1251
1252         url = "https://dyndns.variomedia.de/nic/update"
1253
1254         def prepare_request_data(self, proto):
1255                 data = {
1256                         "hostname" : self.hostname,
1257                         "myip"     : self.get_address(proto),
1258                 }
1259
1260                 return data
1261
1262
1263 class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
1264         handle    = "zoneedit.com"
1265         name      = "Zoneedit"
1266         website   = "http://www.zoneedit.com"
1267         protocols = ("ipv4",)
1268
1269         # Detailed information about the request and the response codes can be
1270         # obtained here:
1271         # http://www.zoneedit.com/doc/api/other.html
1272         # http://www.zoneedit.com/faq.html
1273
1274         url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1275
1276         def update_protocol(self, proto):
1277                 data = {
1278                         "dnsto" : self.get_address(proto),
1279                         "host"  : self.hostname
1280                 }
1281
1282                 # Send update to the server.
1283                 response = self.send_request(self.url, username=self.username, password=self.password,
1284                         data=data)
1285
1286                 # Get the full response message.
1287                 output = response.read()
1288
1289                 # Handle success messages.
1290                 if output.startswith("<SUCCESS"):
1291                         return
1292
1293                 # Handle error codes.
1294                 if output.startswith("invalid login"):
1295                         raise DDNSAuthenticationError
1296                 elif output.startswith("<ERROR CODE=\"704\""):
1297                         raise DDNSRequestError(_("No valid FQDN was given.")) 
1298                 elif output.startswith("<ERROR CODE=\"702\""):
1299                         raise DDNSInternalServerError
1300
1301                 # If we got here, some other update error happened.
1302                 raise DDNSUpdateError
1303
1304
1305 class DDNSProviderZZZZ(DDNSProvider):
1306         handle    = "zzzz.io"
1307         name      = "zzzz"
1308         website   = "https://zzzz.io"
1309         protocols = ("ipv6", "ipv4",)
1310
1311         # Detailed information about the update request can be found here:
1312         # https://zzzz.io/faq/
1313
1314         # Details about the possible response codes have been provided in the bugtracker:
1315         # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1316
1317         url = "https://zzzz.io/api/v1/update"
1318         can_remove_records = False
1319
1320         def update_protocol(self, proto):
1321                 data = {
1322                         "ip"    : self.get_address(proto),
1323                         "token" : self.token,
1324                 }
1325
1326                 if proto == "ipv6":
1327                         data["type"] = "aaaa"
1328
1329                 # zzzz uses the host from the full hostname as part
1330                 # of the update url.
1331                 host, domain = self.hostname.split(".", 1)
1332
1333                 # Add host value to the update url.
1334                 url = "%s/%s" % (self.url, host)
1335
1336                 # Send update to the server.
1337                 try:
1338                         response = self.send_request(url, data=data)
1339
1340                 # Handle error codes.
1341                 except DDNSNotFound:
1342                         raise DDNSRequestError(_("Invalid hostname specified"))
1343
1344                 # Handle success messages.
1345                 if response.code == 200:
1346                         return
1347
1348                 # If we got here, some other update error happened.
1349                 raise DDNSUpdateError