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