]> git.ipfire.org Git - ddns.git/blob - src/ddns/providers.py
Merge remote-tracking branch 'stevee/twodns.de'
[ddns.git] / src / ddns / providers.py
1 #!/usr/bin/python
2 ###############################################################################
3 # #
4 # ddns - A dynamic DNS client for IPFire #
5 # Copyright (C) 2012 IPFire development team #
6 # #
7 # This program is free software: you can redistribute it and/or modify #
8 # it under the terms of the GNU General Public License as published by #
9 # the Free Software Foundation, either version 3 of the License, or #
10 # (at your option) any later version. #
11 # #
12 # This program is distributed in the hope that it will be useful, #
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15 # GNU General Public License for more details. #
16 # #
17 # You should have received a copy of the GNU General Public License #
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
19 # #
20 ###############################################################################
21
22 import logging
23 import urllib2
24 import xml.dom.minidom
25
26 from i18n import _
27
28 # Import all possible exception types.
29 from .errors import *
30
31 logger = logging.getLogger("ddns.providers")
32 logger.propagate = 1
33
34 class DDNSProvider(object):
35 INFO = {
36 # A short string that uniquely identifies
37 # this provider.
38 "handle" : None,
39
40 # The full name of the provider.
41 "name" : None,
42
43 # A weburl to the homepage of the provider.
44 # (Where to register a new account?)
45 "website" : None,
46
47 # A list of supported protocols.
48 "protocols" : ["ipv6", "ipv4"],
49 }
50
51 DEFAULT_SETTINGS = {}
52
53 def __init__(self, core, **settings):
54 self.core = core
55
56 # Copy a set of default settings and
57 # update them by those from the configuration file.
58 self.settings = self.DEFAULT_SETTINGS.copy()
59 self.settings.update(settings)
60
61 def __repr__(self):
62 return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
63
64 def __cmp__(self, other):
65 return cmp(self.hostname, other.hostname)
66
67 @property
68 def name(self):
69 """
70 Returns the name of the provider.
71 """
72 return self.INFO.get("name")
73
74 @property
75 def website(self):
76 """
77 Returns the website URL of the provider
78 or None if that is not available.
79 """
80 return self.INFO.get("website", None)
81
82 @property
83 def handle(self):
84 """
85 Returns the handle of this provider.
86 """
87 return self.INFO.get("handle")
88
89 def get(self, key, default=None):
90 """
91 Get a setting from the settings dictionary.
92 """
93 return self.settings.get(key, default)
94
95 @property
96 def hostname(self):
97 """
98 Fast access to the hostname.
99 """
100 return self.get("hostname")
101
102 @property
103 def username(self):
104 """
105 Fast access to the username.
106 """
107 return self.get("username")
108
109 @property
110 def password(self):
111 """
112 Fast access to the password.
113 """
114 return self.get("password")
115
116 @property
117 def protocols(self):
118 return self.INFO.get("protocols")
119
120 @property
121 def token(self):
122 """
123 Fast access to the token.
124 """
125 return self.get("token")
126
127 def __call__(self, force=False):
128 if force:
129 logger.info(_("Updating %s forced") % self.hostname)
130
131 # Check if we actually need to update this host.
132 elif self.is_uptodate(self.protocols):
133 logger.info(_("%s is already up to date") % self.hostname)
134 return
135
136 # Execute the update.
137 self.update()
138
139 def update(self):
140 raise NotImplementedError
141
142 def is_uptodate(self, protos):
143 """
144 Returns True if this host is already up to date
145 and does not need to change the IP address on the
146 name server.
147 """
148 for proto in protos:
149 addresses = self.core.system.resolve(self.hostname, proto)
150
151 current_address = self.get_address(proto)
152
153 if not current_address in addresses:
154 return False
155
156 return True
157
158 def send_request(self, *args, **kwargs):
159 """
160 Proxy connection to the send request
161 method.
162 """
163 return self.core.system.send_request(*args, **kwargs)
164
165 def get_address(self, proto):
166 """
167 Proxy method to get the current IP address.
168 """
169 return self.core.system.get_address(proto)
170
171
172 class DDNSProviderAllInkl(DDNSProvider):
173 INFO = {
174 "handle" : "all-inkl.com",
175 "name" : "All-inkl.com",
176 "website" : "http://all-inkl.com/",
177 "protocols" : ["ipv4",]
178 }
179
180 # There are only information provided by the vendor how to
181 # perform an update on a FRITZ Box. Grab requried informations
182 # from the net.
183 # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
184
185 url = "http://dyndns.kasserver.com"
186
187 def update(self):
188
189 # There is no additional data required so we directly can
190 # send our request.
191 try:
192 # Send request to the server.
193 response = self.send_request(self.url, username=self.username, password=self.password)
194
195 # Handle 401 HTTP Header (Authentication Error)
196 except urllib2.HTTPError, e:
197 if e.code == 401:
198 raise DDNSAuthenticationError
199
200 raise
201
202 # Get the full response message.
203 output = response.read()
204
205 # Handle success messages.
206 if output.startswith("good") or output.startswith("nochg"):
207 return
208
209 # If we got here, some other update error happened.
210 raise DDNSUpdateError
211
212
213 class DDNSProviderDHS(DDNSProvider):
214 INFO = {
215 "handle" : "dhs.org",
216 "name" : "DHS International",
217 "website" : "http://dhs.org/",
218 "protocols" : ["ipv4",]
219 }
220
221 # No information about the used update api provided on webpage,
222 # grabed from source code of ez-ipudate.
223 url = "http://members.dhs.org/nic/hosts"
224
225 def update(self):
226 data = {
227 "domain" : self.hostname,
228 "ip" : self.get_address("ipv4"),
229 "hostcmd" : "edit",
230 "hostcmdstage" : "2",
231 "type" : "4",
232 }
233
234 # Send update to the server.
235 response = self.send_request(self.url, username=self.username, password=self.password,
236 data=data)
237
238 # Handle success messages.
239 if response.code == 200:
240 return
241
242 # Handle error codes.
243 elif response.code == 401:
244 raise DDNSAuthenticationError
245
246 # If we got here, some other update error happened.
247 raise DDNSUpdateError
248
249
250 class DDNSProviderDNSpark(DDNSProvider):
251 INFO = {
252 "handle" : "dnspark.com",
253 "name" : "DNS Park",
254 "website" : "http://dnspark.com/",
255 "protocols" : ["ipv4",]
256 }
257
258 # Informations to the used api can be found here:
259 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
260 url = "https://control.dnspark.com/api/dynamic/update.php"
261
262 def update(self):
263 data = {
264 "domain" : self.hostname,
265 "ip" : self.get_address("ipv4"),
266 }
267
268 # Send update to the server.
269 response = self.send_request(self.url, username=self.username, password=self.password,
270 data=data)
271
272 # Get the full response message.
273 output = response.read()
274
275 # Handle success messages.
276 if output.startswith("ok") or output.startswith("nochange"):
277 return
278
279 # Handle error codes.
280 if output == "unauth":
281 raise DDNSAuthenticationError
282 elif output == "abuse":
283 raise DDNSAbuseError
284 elif output == "blocked":
285 raise DDNSBlockedError
286 elif output == "nofqdn":
287 raise DDNSRequestError(_("No valid FQDN was given."))
288 elif output == "nohost":
289 raise DDNSRequestError(_("Invalid hostname specified."))
290 elif output == "notdyn":
291 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
292 elif output == "invalid":
293 raise DDNSRequestError(_("Invalid IP address has been sent."))
294
295 # If we got here, some other update error happened.
296 raise DDNSUpdateError
297
298
299 class DDNSProviderDtDNS(DDNSProvider):
300 INFO = {
301 "handle" : "dtdns.com",
302 "name" : "DtDNS",
303 "website" : "http://dtdns.com/",
304 "protocols" : ["ipv4",]
305 }
306
307 # Information about the format of the HTTPS request is to be found
308 # http://www.dtdns.com/dtsite/updatespec
309 url = "https://www.dtdns.com/api/autodns.cfm"
310
311 def update(self):
312 data = {
313 "ip" : self.get_address("ipv4"),
314 "id" : self.hostname,
315 "pw" : self.password
316 }
317
318 # Send update to the server.
319 response = self.send_request(self.url, data=data)
320
321 # Get the full response message.
322 output = response.read()
323
324 # Remove all leading and trailing whitespace.
325 output = output.strip()
326
327 # Handle success messages.
328 if "now points to" in output:
329 return
330
331 # Handle error codes.
332 if output == "No hostname to update was supplied.":
333 raise DDNSRequestError(_("No hostname specified."))
334
335 elif output == "The hostname you supplied is not valid.":
336 raise DDNSRequestError(_("Invalid hostname specified."))
337
338 elif output == "The password you supplied is not valid.":
339 raise DDNSAuthenticationError
340
341 elif output == "Administration has disabled this account.":
342 raise DDNSRequestError(_("Account has been disabled."))
343
344 elif output == "Illegal character in IP.":
345 raise DDNSRequestError(_("Invalid IP address has been sent."))
346
347 elif output == "Too many failed requests.":
348 raise DDNSRequestError(_("Too many failed requests."))
349
350 # If we got here, some other update error happened.
351 raise DDNSUpdateError
352
353
354 class DDNSProviderDynDNS(DDNSProvider):
355 INFO = {
356 "handle" : "dyndns.org",
357 "name" : "Dyn",
358 "website" : "http://dyn.com/dns/",
359 "protocols" : ["ipv4",]
360 }
361
362 # Information about the format of the request is to be found
363 # http://http://dyn.com/support/developers/api/perform-update/
364 # http://dyn.com/support/developers/api/return-codes/
365 url = "https://members.dyndns.org/nic/update"
366
367 def _prepare_request_data(self):
368 data = {
369 "hostname" : self.hostname,
370 "myip" : self.get_address("ipv4"),
371 }
372
373 return data
374
375 def update(self):
376 data = self._prepare_request_data()
377
378 # Send update to the server.
379 response = self.send_request(self.url, data=data,
380 username=self.username, password=self.password)
381
382 # Get the full response message.
383 output = response.read()
384
385 # Handle success messages.
386 if output.startswith("good") or output.startswith("nochg"):
387 return
388
389 # Handle error codes.
390 if output == "badauth":
391 raise DDNSAuthenticationError
392 elif output == "aduse":
393 raise DDNSAbuseError
394 elif output == "notfqdn":
395 raise DDNSRequestError(_("No valid FQDN was given."))
396 elif output == "nohost":
397 raise DDNSRequestError(_("Specified host does not exist."))
398 elif output == "911":
399 raise DDNSInternalServerError
400 elif output == "dnserr":
401 raise DDNSInternalServerError(_("DNS error encountered."))
402
403 # If we got here, some other update error happened.
404 raise DDNSUpdateError
405
406
407 class DDNSProviderDynU(DDNSProviderDynDNS):
408 INFO = {
409 "handle" : "dynu.com",
410 "name" : "Dynu",
411 "website" : "http://dynu.com/",
412 "protocols" : ["ipv6", "ipv4",]
413 }
414
415
416 # Detailed information about the request and response codes
417 # are available on the providers webpage.
418 # http://dynu.com/Default.aspx?page=dnsapi
419
420 url = "https://api.dynu.com/nic/update"
421
422 def _prepare_request_data(self):
423 data = DDNSProviderDynDNS._prepare_request_data(self)
424
425 # This one supports IPv6
426 data.update({
427 "myipv6" : self.get_address("ipv6"),
428 })
429
430 return data
431
432
433 class DDNSProviderEasyDNS(DDNSProviderDynDNS):
434 INFO = {
435 "handle" : "easydns.com",
436 "name" : "EasyDNS",
437 "website" : "http://www.easydns.com/",
438 "protocols" : ["ipv4",]
439 }
440
441 # There is only some basic documentation provided by the vendor,
442 # also searching the web gain very poor results.
443 # http://mediawiki.easydns.com/index.php/Dynamic_DNS
444
445 url = "http://api.cp.easydns.com/dyn/tomato.php"
446
447
448 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
449 INFO = {
450 "handle" : "freedns.afraid.org",
451 "name" : "freedns.afraid.org",
452 "website" : "http://freedns.afraid.org/",
453 "protocols" : ["ipv6", "ipv4",]
454 }
455
456 # No information about the request or response could be found on the vendor
457 # page. All used values have been collected by testing.
458 url = "https://freedns.afraid.org/dynamic/update.php"
459
460 @property
461 def proto(self):
462 return self.get("proto")
463
464 def update(self):
465 address = self.get_address(self.proto)
466
467 data = {
468 "address" : address,
469 }
470
471 # Add auth token to the update url.
472 url = "%s?%s" % (self.url, self.token)
473
474 # Send update to the server.
475 response = self.send_request(url, data=data)
476
477 if output.startswith("Updated") or "has not changed" in output:
478 return
479
480 # Handle error codes.
481 if output == "ERROR: Unable to locate this record":
482 raise DDNSAuthenticationError
483 elif "is an invalid IP address" in output:
484 raise DDNSRequestError(_("Invalid IP address has been sent."))
485
486
487 class DDNSProviderLightningWireLabs(DDNSProvider):
488 INFO = {
489 "handle" : "dns.lightningwirelabs.com",
490 "name" : "Lightning Wire Labs",
491 "website" : "http://dns.lightningwirelabs.com/",
492 "protocols" : ["ipv6", "ipv4",]
493 }
494
495 # Information about the format of the HTTPS request is to be found
496 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
497 url = "https://dns.lightningwirelabs.com/update"
498
499 def update(self):
500 data = {
501 "hostname" : self.hostname,
502 }
503
504 # Check if we update an IPv6 address.
505 address6 = self.get_address("ipv6")
506 if address6:
507 data["address6"] = address6
508
509 # Check if we update an IPv4 address.
510 address4 = self.get_address("ipv4")
511 if address4:
512 data["address4"] = address4
513
514 # Raise an error if none address is given.
515 if not data.has_key("address6") and not data.has_key("address4"):
516 raise DDNSConfigurationError
517
518 # Check if a token has been set.
519 if self.token:
520 data["token"] = self.token
521
522 # Check for username and password.
523 elif self.username and self.password:
524 data.update({
525 "username" : self.username,
526 "password" : self.password,
527 })
528
529 # Raise an error if no auth details are given.
530 else:
531 raise DDNSConfigurationError
532
533 # Send update to the server.
534 response = self.send_request(self.url, data=data)
535
536 # Handle success messages.
537 if response.code == 200:
538 return
539
540 # Handle error codes.
541 if response.code == 403:
542 raise DDNSAuthenticationError
543 elif response.code == 400:
544 raise DDNSRequestError
545
546 # If we got here, some other update error happened.
547 raise DDNSUpdateError
548
549
550 class DDNSProviderNamecheap(DDNSProvider):
551 INFO = {
552 "handle" : "namecheap.com",
553 "name" : "Namecheap",
554 "website" : "http://namecheap.com",
555 "protocols" : ["ipv4",]
556 }
557
558 # Information about the format of the HTTP request is to be found
559 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
560 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
561
562 url = "https://dynamicdns.park-your-domain.com/update"
563
564 def parse_xml(self, document, content):
565 # Send input to the parser.
566 xmldoc = xml.dom.minidom.parseString(document)
567
568 # Get XML elements by the given content.
569 element = xmldoc.getElementsByTagName(content)
570
571 # If no element has been found, we directly can return None.
572 if not element:
573 return None
574
575 # Only get the first child from an element, even there are more than one.
576 firstchild = element[0].firstChild
577
578 # Get the value of the child.
579 value = firstchild.nodeValue
580
581 # Return the value.
582 return value
583
584 def update(self):
585 # Namecheap requires the hostname splitted into a host and domain part.
586 host, domain = self.hostname.split(".", 1)
587
588 data = {
589 "ip" : self.get_address("ipv4"),
590 "password" : self.password,
591 "host" : host,
592 "domain" : domain
593 }
594
595 # Send update to the server.
596 response = self.send_request(self.url, data=data)
597
598 # Get the full response message.
599 output = response.read()
600
601 # Handle success messages.
602 if self.parse_xml(output, "IP") == self.get_address("ipv4"):
603 return
604
605 # Handle error codes.
606 errorcode = self.parse_xml(output, "ResponseNumber")
607
608 if errorcode == "304156":
609 raise DDNSAuthenticationError
610 elif errorcode == "316153":
611 raise DDNSRequestError(_("Domain not found."))
612 elif errorcode == "316154":
613 raise DDNSRequestError(_("Domain not active."))
614 elif errorcode in ("380098", "380099"):
615 raise DDNSInternalServerError
616
617 # If we got here, some other update error happened.
618 raise DDNSUpdateError
619
620
621 class DDNSProviderNOIP(DDNSProviderDynDNS):
622 INFO = {
623 "handle" : "no-ip.com",
624 "name" : "No-IP",
625 "website" : "http://www.no-ip.com/",
626 "protocols" : ["ipv4",]
627 }
628
629 # Information about the format of the HTTP request is to be found
630 # here: http://www.no-ip.com/integrate/request and
631 # here: http://www.no-ip.com/integrate/response
632
633 url = "http://dynupdate.no-ip.com/nic/update"
634
635 def _prepare_request_data(self):
636 data = {
637 "hostname" : self.hostname,
638 "address" : self.get_address("ipv4"),
639 }
640
641 return data
642
643
644 class DDNSProviderOVH(DDNSProviderDynDNS):
645 INFO = {
646 "handle" : "ovh.com",
647 "name" : "OVH",
648 "website" : "http://www.ovh.com/",
649 "protocols" : ["ipv4",]
650 }
651
652 # OVH only provides very limited information about how to
653 # update a DynDNS host. They only provide the update url
654 # on the their german subpage.
655 #
656 # http://hilfe.ovh.de/DomainDynHost
657
658 url = "https://www.ovh.com/nic/update"
659
660 def _prepare_request_data(self):
661 data = DDNSProviderDynDNS._prepare_request_data(self)
662 data.update({
663 "system" : "dyndns",
664 })
665
666 return data
667
668
669 class DDNSProviderRegfish(DDNSProvider):
670 INFO = {
671 "handle" : "regfish.com",
672 "name" : "Regfish GmbH",
673 "website" : "http://www.regfish.com/",
674 "protocols" : ["ipv6", "ipv4",]
675 }
676
677 # A full documentation to the providers api can be found here
678 # but is only available in german.
679 # https://www.regfish.de/domains/dyndns/dokumentation
680
681 url = "https://dyndns.regfish.de/"
682
683 def update(self):
684 data = {
685 "fqdn" : self.hostname,
686 }
687
688 # Check if we update an IPv6 address.
689 address6 = self.get_address("ipv6")
690 if address6:
691 data["ipv6"] = address6
692
693 # Check if we update an IPv4 address.
694 address4 = self.get_address("ipv4")
695 if address4:
696 data["ipv4"] = address4
697
698 # Raise an error if none address is given.
699 if not data.has_key("ipv6") and not data.has_key("ipv4"):
700 raise DDNSConfigurationError
701
702 # Check if a token has been set.
703 if self.token:
704 data["token"] = self.token
705
706 # Raise an error if no token and no useranem and password
707 # are given.
708 elif not self.username and not self.password:
709 raise DDNSConfigurationError(_("No Auth details specified."))
710
711 # HTTP Basic Auth is only allowed if no token is used.
712 if self.token:
713 # Send update to the server.
714 response = self.send_request(self.url, data=data)
715 else:
716 # Send update to the server.
717 response = self.send_request(self.url, username=self.username, password=self.password,
718 data=data)
719
720 # Get the full response message.
721 output = response.read()
722
723 # Handle success messages.
724 if "100" in output or "101" in output:
725 return
726
727 # Handle error codes.
728 if "401" or "402" in output:
729 raise DDNSAuthenticationError
730 elif "408" in output:
731 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
732 elif "409" in output:
733 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
734 elif "412" in output:
735 raise DDNSRequestError(_("No valid FQDN was given."))
736 elif "414" in output:
737 raise DDNSInternalServerError
738
739 # If we got here, some other update error happened.
740 raise DDNSUpdateError
741
742
743 class DDNSProviderSelfhost(DDNSProvider):
744 INFO = {
745 "handle" : "selfhost.de",
746 "name" : "Selfhost.de",
747 "website" : "http://www.selfhost.de/",
748 "protocols" : ["ipv4",],
749 }
750
751 url = "https://carol.selfhost.de/update"
752
753 def update(self):
754 data = {
755 "username" : self.username,
756 "password" : self.password,
757 "textmodi" : "1",
758 }
759
760 response = self.send_request(self.url, data=data)
761
762 match = re.search("status=20(0|4)", response.read())
763 if not match:
764 raise DDNSUpdateError
765
766
767 class DDNSProviderSPDNS(DDNSProviderDynDNS):
768 INFO = {
769 "handle" : "spdns.org",
770 "name" : "SPDNS",
771 "website" : "http://spdns.org/",
772 "protocols" : ["ipv4",]
773 }
774
775 # Detailed information about request and response codes are provided
776 # by the vendor. They are using almost the same mechanism and status
777 # codes as dyndns.org so we can inherit all those stuff.
778 #
779 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
780 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
781
782 url = "https://update.spdns.de/nic/update"
783
784
785 class DDNSProviderStrato(DDNSProviderDynDNS):
786 INFO = {
787 "handle" : "strato.com",
788 "name" : "Strato AG",
789 "website" : "http:/www.strato.com/",
790 "protocols" : ["ipv4",]
791 }
792
793 # Information about the request and response can be obtained here:
794 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
795
796 url = "https://dyndns.strato.com/nic/update"
797
798
799 class DDNSProviderTwoDNS(DDNSProviderDynDNS):
800 INFO = {
801 "handle" : "twodns.de",
802 "name" : "TwoDNS",
803 "website" : "http://www.twodns.de",
804 "protocols" : ["ipv4",]
805 }
806
807 # Detailed information about the request can be found here
808 # http://twodns.de/en/faqs
809 # http://twodns.de/en/api
810
811 url = "https://update.twodns.de/update"
812
813 def _prepare_request_data(self):
814 data = {
815 "ip" : self.get_address("ipv4"),
816 "hostname" : self.hostname
817 }
818
819 return data
820
821
822 class DDNSProviderVariomedia(DDNSProviderDynDNS):
823 INFO = {
824 "handle" : "variomedia.de",
825 "name" : "Variomedia",
826 "website" : "http://www.variomedia.de/",
827 "protocols" : ["ipv6", "ipv4",]
828 }
829
830 # Detailed information about the request can be found here
831 # https://dyndns.variomedia.de/
832
833 url = "https://dyndns.variomedia.de/nic/update"
834
835 @property
836 def proto(self):
837 return self.get("proto")
838
839 def _prepare_request_data(self):
840 data = {
841 "hostname" : self.hostname,
842 "myip" : self.get_address(self.proto)
843 }
844
845 return data