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