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