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