Add Namecheap as new provider.
[oddments/ddns.git] / src / ddns / providers.py
CommitLineData
f22ab085 1#!/usr/bin/python
3fdcb9d1
MT
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###############################################################################
f22ab085 21
7399fc5b 22import logging
d1cd57eb 23import xml.dom.minidom
7399fc5b
MT
24
25from i18n import _
26
f22ab085
MT
27# Import all possible exception types.
28from .errors import *
29
7399fc5b
MT
30logger = logging.getLogger("ddns.providers")
31logger.propagate = 1
32
f22ab085
MT
33class 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
7399fc5b
MT
115 @property
116 def protocols(self):
117 return self.INFO.get("protocols")
118
46687828
SS
119 @property
120 def token(self):
121 """
122 Fast access to the token.
123 """
124 return self.get("token")
125
9da3e685
MT
126 def __call__(self, force=False):
127 if force:
128 logger.info(_("Updating %s forced") % self.hostname)
129
7399fc5b 130 # Check if we actually need to update this host.
9da3e685 131 elif self.is_uptodate(self.protocols):
7399fc5b
MT
132 logger.info(_("%s is already up to date") % self.hostname)
133 return
134
135 # Execute the update.
5f402f36
MT
136 self.update()
137
138 def update(self):
f22ab085
MT
139 raise NotImplementedError
140
7399fc5b
MT
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
f22ab085
MT
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
f3cf1f70
SS
171class 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
5f402f36 183 def update(self):
f3cf1f70
SS
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.
175c9b80 193 response = self.send_request(self.url, username=self.username, password=self.password,
f3cf1f70
SS
194 data=data)
195
196 # Handle success messages.
197 if response.code == 200:
198 return
199
200 # Handle error codes.
4caed6ed 201 elif response.code == 401:
f3cf1f70
SS
202 raise DDNSAuthenticationError
203
204 # If we got here, some other update error happened.
205 raise DDNSUpdateError
206
207
39301272
SS
208class 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
5f402f36 220 def update(self):
39301272
SS
221 data = {
222 "domain" : self.hostname,
223 "ip" : self.get_address("ipv4"),
224 }
225
226 # Send update to the server.
175c9b80 227 response = self.send_request(self.url, username=self.username, password=self.password,
39301272
SS
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
43b2cd59
SS
256
257class 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
43b2cd59
SS
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
bfed6701
SS
312class 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
88f39629 325 def _prepare_request_data(self):
bfed6701
SS
326 data = {
327 "hostname" : self.hostname,
328 "myip" : self.get_address("ipv4"),
329 }
330
88f39629
SS
331 return data
332
333 def update(self):
334 data = self._prepare_request_data()
335
bfed6701 336 # Send update to the server.
88f39629
SS
337 response = self.send_request(self.url, data=data,
338 username=self.username, password=self.password)
bfed6701
SS
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
3a8407fa
SS
365class 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):
54d3efc8
MT
381 data = DDNSProviderDynDNS._prepare_request_data(self)
382
383 # This one supports IPv6
384 data.update({
3a8407fa 385 "myipv6" : self.get_address("ipv6"),
54d3efc8
MT
386 })
387
388 return data
3a8407fa
SS
389
390
ee071271
SS
391class 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
aa21a4c6
SS
406class 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
aa21a4c6
SS
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
aa21a4c6 444
a08c1b72
SS
445class 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
5f402f36 457 def update(self):
a08c1b72
SS
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.
cb455540 492 response = self.send_request(self.url, data=data)
a08c1b72
SS
493
494 # Handle success messages.
495 if response.code == 200:
496 return
497
498 # Handle error codes.
2e5ad318 499 if response.code == 403:
a08c1b72 500 raise DDNSAuthenticationError
2e5ad318 501 elif response.code == 400:
a08c1b72
SS
502 raise DDNSRequestError
503
504 # If we got here, some other update error happened.
505 raise DDNSUpdateError
506
507
d1cd57eb
SS
508class 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
88f39629 579class DDNSProviderNOIP(DDNSProviderDynDNS):
f22ab085
MT
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
88f39629 591 url = "http://dynupdate.no-ip.com/nic/update"
2de06f59 592
88f39629 593 def _prepare_request_data(self):
2de06f59
MT
594 data = {
595 "hostname" : self.hostname,
f22ab085
MT
596 "address" : self.get_address("ipv4"),
597 }
598
88f39629 599 return data
f22ab085
MT
600
601
a508bda6
SS
602class 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):
54d3efc8
MT
619 data = DDNSProviderDynDNS._prepare_request_data(self)
620 data.update({
621 "system" : "dyndns",
622 })
623
624 return data
a508bda6
SS
625
626
ef33455e
SS
627class 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
f22ab085
MT
701class DDNSProviderSelfhost(DDNSProvider):
702 INFO = {
703 "handle" : "selfhost.de",
704 "name" : "Selfhost.de",
705 "website" : "http://www.selfhost.de/",
706 "protocols" : ["ipv4",],
707 }
708
2de06f59 709 url = "https://carol.selfhost.de/update"
f22ab085 710
5f402f36 711 def update(self):
2de06f59
MT
712 data = {
713 "username" : self.username,
714 "password" : self.password,
715 "textmodi" : "1",
716 }
f22ab085 717
2de06f59 718 response = self.send_request(self.url, data=data)
f22ab085
MT
719
720 match = re.search("status=20(0|4)", response.read())
721 if not match:
722 raise DDNSUpdateError
b09b1545
SS
723
724
725class 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"
4ec90b93
MT
741
742
c8c7ca8f
SS
743class 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 }
54d3efc8
MT
765
766 return data