]> git.ipfire.org Git - ddns.git/blob - src/ddns/providers.py
383d7995b8e0fd637597ca632248b8cfec9c6260
[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
431 class DDNSProviderLightningWireLabs(DDNSProvider):
432 handle = "dns.lightningwirelabs.com"
433 name = "Lightning Wire Labs DNS Service"
434 website = "http://dns.lightningwirelabs.com/"
435
436 # Information about the format of the HTTPS request is to be found
437 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
438 url = "https://dns.lightningwirelabs.com/update"
439
440 def update(self):
441 data = {
442 "hostname" : self.hostname,
443 "address6" : self.get_address("ipv6", "-"),
444 "address4" : self.get_address("ipv4", "-"),
445 }
446
447 # Check if a token has been set.
448 if self.token:
449 data["token"] = self.token
450
451 # Check for username and password.
452 elif self.username and self.password:
453 data.update({
454 "username" : self.username,
455 "password" : self.password,
456 })
457
458 # Raise an error if no auth details are given.
459 else:
460 raise DDNSConfigurationError
461
462 # Send update to the server.
463 response = self.send_request(self.url, data=data)
464
465 # Handle success messages.
466 if response.code == 200:
467 return
468
469 # If we got here, some other update error happened.
470 raise DDNSUpdateError
471
472
473 class DDNSProviderNamecheap(DDNSProvider):
474 handle = "namecheap.com"
475 name = "Namecheap"
476 website = "http://namecheap.com"
477 protocols = ("ipv4",)
478
479 # Information about the format of the HTTP request is to be found
480 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
481 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
482
483 url = "https://dynamicdns.park-your-domain.com/update"
484
485 def parse_xml(self, document, content):
486 # Send input to the parser.
487 xmldoc = xml.dom.minidom.parseString(document)
488
489 # Get XML elements by the given content.
490 element = xmldoc.getElementsByTagName(content)
491
492 # If no element has been found, we directly can return None.
493 if not element:
494 return None
495
496 # Only get the first child from an element, even there are more than one.
497 firstchild = element[0].firstChild
498
499 # Get the value of the child.
500 value = firstchild.nodeValue
501
502 # Return the value.
503 return value
504
505 def update(self):
506 # Namecheap requires the hostname splitted into a host and domain part.
507 host, domain = self.hostname.split(".", 1)
508
509 data = {
510 "ip" : self.get_address("ipv4"),
511 "password" : self.password,
512 "host" : host,
513 "domain" : domain
514 }
515
516 # Send update to the server.
517 response = self.send_request(self.url, data=data)
518
519 # Get the full response message.
520 output = response.read()
521
522 # Handle success messages.
523 if self.parse_xml(output, "IP") == self.get_address("ipv4"):
524 return
525
526 # Handle error codes.
527 errorcode = self.parse_xml(output, "ResponseNumber")
528
529 if errorcode == "304156":
530 raise DDNSAuthenticationError
531 elif errorcode == "316153":
532 raise DDNSRequestError(_("Domain not found."))
533 elif errorcode == "316154":
534 raise DDNSRequestError(_("Domain not active."))
535 elif errorcode in ("380098", "380099"):
536 raise DDNSInternalServerError
537
538 # If we got here, some other update error happened.
539 raise DDNSUpdateError
540
541
542 class DDNSProviderNOIP(DDNSProviderDynDNS):
543 handle = "no-ip.com"
544 name = "No-IP"
545 website = "http://www.no-ip.com/"
546
547 # Information about the format of the HTTP request is to be found
548 # here: http://www.no-ip.com/integrate/request and
549 # here: http://www.no-ip.com/integrate/response
550
551 url = "http://dynupdate.no-ip.com/nic/update"
552
553 def _prepare_request_data(self):
554 data = {
555 "hostname" : self.hostname,
556 "address" : self.get_address("ipv4"),
557 }
558
559 return data
560
561
562 class DDNSProviderOVH(DDNSProviderDynDNS):
563 handle = "ovh.com"
564 name = "OVH"
565 website = "http://www.ovh.com/"
566
567 # OVH only provides very limited information about how to
568 # update a DynDNS host. They only provide the update url
569 # on the their german subpage.
570 #
571 # http://hilfe.ovh.de/DomainDynHost
572
573 url = "https://www.ovh.com/nic/update"
574
575 def _prepare_request_data(self):
576 data = DDNSProviderDynDNS._prepare_request_data(self)
577 data.update({
578 "system" : "dyndns",
579 })
580
581 return data
582
583
584 class DDNSProviderRegfish(DDNSProvider):
585 handle = "regfish.com"
586 name = "Regfish GmbH"
587 website = "http://www.regfish.com/"
588
589 # A full documentation to the providers api can be found here
590 # but is only available in german.
591 # https://www.regfish.de/domains/dyndns/dokumentation
592
593 url = "https://dyndns.regfish.de/"
594
595 def update(self):
596 data = {
597 "fqdn" : self.hostname,
598 }
599
600 # Check if we update an IPv6 address.
601 address6 = self.get_address("ipv6")
602 if address6:
603 data["ipv6"] = address6
604
605 # Check if we update an IPv4 address.
606 address4 = self.get_address("ipv4")
607 if address4:
608 data["ipv4"] = address4
609
610 # Raise an error if none address is given.
611 if not data.has_key("ipv6") and not data.has_key("ipv4"):
612 raise DDNSConfigurationError
613
614 # Check if a token has been set.
615 if self.token:
616 data["token"] = self.token
617
618 # Raise an error if no token and no useranem and password
619 # are given.
620 elif not self.username and not self.password:
621 raise DDNSConfigurationError(_("No Auth details specified."))
622
623 # HTTP Basic Auth is only allowed if no token is used.
624 if self.token:
625 # Send update to the server.
626 response = self.send_request(self.url, data=data)
627 else:
628 # Send update to the server.
629 response = self.send_request(self.url, username=self.username, password=self.password,
630 data=data)
631
632 # Get the full response message.
633 output = response.read()
634
635 # Handle success messages.
636 if "100" in output or "101" in output:
637 return
638
639 # Handle error codes.
640 if "401" or "402" in output:
641 raise DDNSAuthenticationError
642 elif "408" in output:
643 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
644 elif "409" in output:
645 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
646 elif "412" in output:
647 raise DDNSRequestError(_("No valid FQDN was given."))
648 elif "414" in output:
649 raise DDNSInternalServerError
650
651 # If we got here, some other update error happened.
652 raise DDNSUpdateError
653
654
655 class DDNSProviderSelfhost(DDNSProviderDynDNS):
656 handle = "selfhost.de"
657 name = "Selfhost.de"
658 website = "http://www.selfhost.de/"
659
660 url = "https://carol.selfhost.de/nic/update"
661
662 def _prepare_request_data(self):
663 data = DDNSProviderDynDNS._prepare_request_data(self)
664 data.update({
665 "hostname" : "1",
666 })
667
668 return data
669
670
671 class DDNSProviderSPDNS(DDNSProviderDynDNS):
672 handle = "spdns.org"
673 name = "SPDNS"
674 website = "http://spdns.org/"
675
676 # Detailed information about request and response codes are provided
677 # by the vendor. They are using almost the same mechanism and status
678 # codes as dyndns.org so we can inherit all those stuff.
679 #
680 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
681 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
682
683 url = "https://update.spdns.de/nic/update"
684
685
686 class DDNSProviderStrato(DDNSProviderDynDNS):
687 handle = "strato.com"
688 name = "Strato AG"
689 website = "http:/www.strato.com/"
690
691 # Information about the request and response can be obtained here:
692 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
693
694 url = "https://dyndns.strato.com/nic/update"
695
696
697 class DDNSProviderTwoDNS(DDNSProviderDynDNS):
698 handle = "twodns.de"
699 name = "TwoDNS"
700 website = "http://www.twodns.de"
701
702 # Detailed information about the request can be found here
703 # http://twodns.de/en/faqs
704 # http://twodns.de/en/api
705
706 url = "https://update.twodns.de/update"
707
708 def _prepare_request_data(self):
709 data = {
710 "ip" : self.get_address("ipv4"),
711 "hostname" : self.hostname
712 }
713
714 return data
715
716
717 class DDNSProviderUdmedia(DDNSProviderDynDNS):
718 handle = "udmedia.de"
719 name = "Udmedia GmbH"
720 website = "http://www.udmedia.de"
721
722 # Information about the request can be found here
723 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
724
725 url = "https://www.udmedia.de/nic/update"
726
727
728 class DDNSProviderVariomedia(DDNSProviderDynDNS):
729 handle = "variomedia.de"
730 name = "Variomedia"
731 website = "http://www.variomedia.de/"
732 protocols = ("ipv6", "ipv4",)
733
734 # Detailed information about the request can be found here
735 # https://dyndns.variomedia.de/
736
737 url = "https://dyndns.variomedia.de/nic/update"
738
739 @property
740 def proto(self):
741 return self.get("proto")
742
743 def _prepare_request_data(self):
744 data = {
745 "hostname" : self.hostname,
746 "myip" : self.get_address(self.proto)
747 }
748
749 return data
750
751
752 class DDNSProviderZoneedit(DDNSProvider):
753 handle = "zoneedit.com"
754 name = "Zoneedit"
755 website = "http://www.zoneedit.com"
756
757 # Detailed information about the request and the response codes can be
758 # obtained here:
759 # http://www.zoneedit.com/doc/api/other.html
760 # http://www.zoneedit.com/faq.html
761
762 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
763
764 @property
765 def proto(self):
766 return self.get("proto")
767
768 def update(self):
769 data = {
770 "dnsto" : self.get_address(self.proto),
771 "host" : self.hostname
772 }
773
774 # Send update to the server.
775 response = self.send_request(self.url, username=self.username, password=self.password,
776 data=data)
777
778 # Get the full response message.
779 output = response.read()
780
781 # Handle success messages.
782 if output.startswith("<SUCCESS"):
783 return
784
785 # Handle error codes.
786 if output.startswith("invalid login"):
787 raise DDNSAuthenticationError
788 elif output.startswith("<ERROR CODE=\"704\""):
789 raise DDNSRequestError(_("No valid FQDN was given."))
790 elif output.startswith("<ERROR CODE=\"702\""):
791 raise DDNSInternalServerError
792
793 # If we got here, some other update error happened.
794 raise DDNSUpdateError