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