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