Add Namecheap as new provider.
[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 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