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