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