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