f2e470091fa192876143dcc6e5389299ef6a19a1
[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
24 from i18n import _
25
26 # Import all possible exception types.
27 from .errors import *
28
29 logger = logging.getLogger("ddns.providers")
30 logger.propagate = 1
31
32 class DDNSProvider(object):
33         INFO = {
34                 # A short string that uniquely identifies
35                 # this provider.
36                 "handle"    : None,
37
38                 # The full name of the provider.
39                 "name"      : None,
40
41                 # A weburl to the homepage of the provider.
42                 # (Where to register a new account?)
43                 "website"   : None,
44
45                 # A list of supported protocols.
46                 "protocols" : ["ipv6", "ipv4"],
47         }
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         @property
66         def name(self):
67                 """
68                         Returns the name of the provider.
69                 """
70                 return self.INFO.get("name")
71
72         @property
73         def website(self):
74                 """
75                         Returns the website URL of the provider
76                         or None if that is not available.
77                 """
78                 return self.INFO.get("website", None)
79
80         @property
81         def handle(self):
82                 """
83                         Returns the handle of this provider.
84                 """
85                 return self.INFO.get("handle")
86
87         def get(self, key, default=None):
88                 """
89                         Get a setting from the settings dictionary.
90                 """
91                 return self.settings.get(key, default)
92
93         @property
94         def hostname(self):
95                 """
96                         Fast access to the hostname.
97                 """
98                 return self.get("hostname")
99
100         @property
101         def username(self):
102                 """
103                         Fast access to the username.
104                 """
105                 return self.get("username")
106
107         @property
108         def password(self):
109                 """
110                         Fast access to the password.
111                 """
112                 return self.get("password")
113
114         @property
115         def protocols(self):
116                 return self.INFO.get("protocols")
117
118         @property
119         def token(self):
120                 """
121                         Fast access to the token.
122                 """
123                 return self.get("token")
124
125         def __call__(self, force=False):
126                 if force:
127                         logger.info(_("Updating %s forced") % self.hostname)
128
129                 # Check if we actually need to update this host.
130                 elif self.is_uptodate(self.protocols):
131                         logger.info(_("%s is already up to date") % self.hostname)
132                         return
133
134                 # Execute the update.
135                 self.update()
136
137         def update(self):
138                 raise NotImplementedError
139
140         def is_uptodate(self, protos):
141                 """
142                         Returns True if this host is already up to date
143                         and does not need to change the IP address on the
144                         name server.
145                 """
146                 for proto in protos:
147                         addresses = self.core.system.resolve(self.hostname, proto)
148
149                         current_address = self.get_address(proto)
150
151                         if not current_address in addresses:
152                                 return False
153
154                 return True
155
156         def send_request(self, *args, **kwargs):
157                 """
158                         Proxy connection to the send request
159                         method.
160                 """
161                 return self.core.system.send_request(*args, **kwargs)
162
163         def get_address(self, proto):
164                 """
165                         Proxy method to get the current IP address.
166                 """
167                 return self.core.system.get_address(proto)
168
169
170 class DDNSProviderDHS(DDNSProvider):
171         INFO = {
172                 "handle"    : "dhs.org",
173                 "name"      : "DHS International",
174                 "website"   : "http://dhs.org/",
175                 "protocols" : ["ipv4",]
176         }
177
178         # No information about the used update api provided on webpage,
179         # grabed from source code of ez-ipudate.
180         url = "http://members.dhs.org/nic/hosts"
181
182         def update(self):
183                 data = {
184                         "domain"       : self.hostname,
185                         "ip"           : self.get_address("ipv4"),
186                         "hostcmd"      : "edit",
187                         "hostcmdstage" : "2",
188                         "type"         : "4",
189                 }
190
191                 # Send update to the server.
192                 response = self.send_request(self.url, username=self.username, password=self.password,
193                         data=data)
194
195                 # Handle success messages.
196                 if response.code == 200:
197                         return
198
199                 # Handle error codes.
200                 elif response.code == 401:
201                         raise DDNSAuthenticationError
202
203                 # If we got here, some other update error happened.
204                 raise DDNSUpdateError
205
206
207 class DDNSProviderDNSpark(DDNSProvider):
208         INFO = {
209                 "handle"    : "dnspark.com",
210                 "name"      : "DNS Park",
211                 "website"   : "http://dnspark.com/",
212                 "protocols" : ["ipv4",]
213         }
214
215         # Informations to the used api can be found here:
216         # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
217         url = "https://control.dnspark.com/api/dynamic/update.php"
218
219         def update(self):
220                 data = {
221                         "domain" : self.hostname,
222                         "ip"     : self.get_address("ipv4"),
223                 }
224
225                 # Send update to the server.
226                 response = self.send_request(self.url, username=self.username, password=self.password,
227                         data=data)
228
229                 # Get the full response message.
230                 output = response.read()
231
232                 # Handle success messages.
233                 if output.startswith("ok") or output.startswith("nochange"):
234                         return
235
236                 # Handle error codes.
237                 if output == "unauth":
238                         raise DDNSAuthenticationError
239                 elif output == "abuse":
240                         raise DDNSAbuseError
241                 elif output == "blocked":
242                         raise DDNSBlockedError
243                 elif output == "nofqdn":
244                         raise DDNSRequestError(_("No valid FQDN was given."))
245                 elif output == "nohost":
246                         raise DDNSRequestError(_("Invalid hostname specified."))
247                 elif output == "notdyn":
248                         raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
249                 elif output == "invalid":
250                         raise DDNSRequestError(_("Invalid IP address has been sent."))
251
252                 # If we got here, some other update error happened.
253                 raise DDNSUpdateError
254
255
256 class DDNSProviderDtDNS(DDNSProvider):
257         INFO = {
258                 "handle"    : "dtdns.com",
259                 "name"      : "DtDNS",
260                 "website"   : "http://dtdns.com/",
261                 "protocols" : ["ipv4",]
262                 }
263
264         # Information about the format of the HTTPS request is to be found
265         # http://www.dtdns.com/dtsite/updatespec
266         url = "https://www.dtdns.com/api/autodns.cfm"
267
268         def update(self):
269                 data = {
270                         "ip" : self.get_address("ipv4"),
271                         "id" : self.hostname,
272                         "pw" : self.password
273                 }
274
275                 # Send update to the server.
276                 response = self.send_request(self.url, data=data)
277
278                 # Get the full response message.
279                 output = response.read()
280
281                 # Remove all leading and trailing whitespace.
282                 output = output.strip()
283
284                 # Handle success messages.
285                 if "now points to" in output:
286                         return
287
288                 # Handle error codes.
289                 if output == "No hostname to update was supplied.":
290                         raise DDNSRequestError(_("No hostname specified."))
291
292                 elif output == "The hostname you supplied is not valid.":
293                         raise DDNSRequestError(_("Invalid hostname specified."))
294
295                 elif output == "The password you supplied is not valid.":
296                         raise DDNSAuthenticationError
297
298                 elif output == "Administration has disabled this account.":
299                         raise DDNSRequestError(_("Account has been disabled."))
300
301                 elif output == "Illegal character in IP.":
302                         raise DDNSRequestError(_("Invalid IP address has been sent."))
303
304                 elif output == "Too many failed requests.":
305                         raise DDNSRequestError(_("Too many failed requests."))
306
307                 # If we got here, some other update error happened.
308                 raise DDNSUpdateError
309
310
311 class DDNSProviderDynDNS(DDNSProvider):
312         INFO = {
313                 "handle"    : "dyndns.org",
314                 "name"      : "Dyn",
315                 "website"   : "http://dyn.com/dns/",
316                 "protocols" : ["ipv4",]
317         }
318
319         # Information about the format of the request is to be found
320         # http://http://dyn.com/support/developers/api/perform-update/
321         # http://dyn.com/support/developers/api/return-codes/
322         url = "https://members.dyndns.org/nic/update"
323
324         def _prepare_request_data(self):
325                 data = {
326                         "hostname" : self.hostname,
327                         "myip"     : self.get_address("ipv4"),
328                 }
329
330                 return data
331
332         def update(self):
333                 data = self._prepare_request_data()
334
335                 # Send update to the server.
336                 response = self.send_request(self.url, data=data,
337                         username=self.username, password=self.password)
338
339                 # Get the full response message.
340                 output = response.read()
341
342                 # Handle success messages.
343                 if output.startswith("good") or output.startswith("nochg"):
344                         return
345
346                 # Handle error codes.
347                 if output == "badauth":
348                         raise DDNSAuthenticationError
349                 elif output == "aduse":
350                         raise DDNSAbuseError
351                 elif output == "notfqdn":
352                         raise DDNSRequestError(_("No valid FQDN was given."))
353                 elif output == "nohost":
354                         raise DDNSRequestError(_("Specified host does not exist."))
355                 elif output == "911":
356                         raise DDNSInternalServerError
357                 elif output == "dnserr":
358                         raise DDNSInternalServerError(_("DNS error encountered."))
359
360                 # If we got here, some other update error happened.
361                 raise DDNSUpdateError
362
363
364 class DDNSProviderDynU(DDNSProviderDynDNS):
365         INFO = {
366                 "handle"    : "dynu.com",
367                 "name"      : "Dynu",
368                 "website"   : "http://dynu.com/",
369                 "protocols" : ["ipv6", "ipv4",]
370         }
371
372
373         # Detailed information about the request and response codes
374         # are available on the providers webpage.
375         # http://dynu.com/Default.aspx?page=dnsapi
376
377         url = "https://api.dynu.com/nic/update"
378
379         def _prepare_request_data(self):
380                 data = {
381                         "hostname" : self.hostname,
382                         "myip"     : self.get_address("ipv4"),
383                         "myipv6"   : self.get_address("ipv6"),
384                 }
385
386
387 class DDNSProviderEasyDNS(DDNSProviderDynDNS):
388         INFO = {
389                 "handle"    : "easydns.com",
390                 "name"      : "EasyDNS",
391                 "website"   : "http://www.easydns.com/",
392                 "protocols" : ["ipv4",]
393         }
394
395         # There is only some basic documentation provided by the vendor,
396         # also searching the web gain very poor results.
397         # http://mediawiki.easydns.com/index.php/Dynamic_DNS
398
399         url = "http://api.cp.easydns.com/dyn/tomato.php"
400
401
402 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
403         INFO = {
404                 "handle"    : "freedns.afraid.org",
405                 "name"      : "freedns.afraid.org",
406                 "website"   : "http://freedns.afraid.org/",
407                 "protocols" : ["ipv6", "ipv4",]
408                 }
409
410         # No information about the request or response could be found on the vendor
411         # page. All used values have been collected by testing.
412         url = "https://freedns.afraid.org/dynamic/update.php"
413
414         @property
415         def proto(self):
416                 return self.get("proto")
417
418         def update(self):
419                 address = self.get_address(self.proto)
420
421                 data = {
422                         "address" : address,
423                 }
424
425                 # Add auth token to the update url.
426                 url = "%s?%s" % (self.url, self.token)
427
428                 # Send update to the server.
429                 response = self.send_request(url, data=data)
430
431                 if output.startswith("Updated") or "has not changed" in output:
432                         return
433
434                 # Handle error codes.
435                 if output == "ERROR: Unable to locate this record":
436                         raise DDNSAuthenticationError
437                 elif "is an invalid IP address" in output:
438                         raise DDNSRequestError(_("Invalid IP address has been sent."))
439
440
441 class DDNSProviderLightningWireLabs(DDNSProvider):
442         INFO = {
443                 "handle"    : "dns.lightningwirelabs.com",
444                 "name"      : "Lightning Wire Labs",
445                 "website"   : "http://dns.lightningwirelabs.com/",
446                 "protocols" : ["ipv6", "ipv4",]
447         }
448
449         # Information about the format of the HTTPS request is to be found
450         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
451         url = "https://dns.lightningwirelabs.com/update"
452
453         def update(self):
454                 data =  {
455                         "hostname" : self.hostname,
456                 }
457
458                 # Check if we update an IPv6 address.
459                 address6 = self.get_address("ipv6")
460                 if address6:
461                         data["address6"] = address6
462
463                 # Check if we update an IPv4 address.
464                 address4 = self.get_address("ipv4")
465                 if address4:
466                         data["address4"] = address4
467
468                 # Raise an error if none address is given.
469                 if not data.has_key("address6") and not data.has_key("address4"):
470                         raise DDNSConfigurationError
471
472                 # Check if a token has been set.
473                 if self.token:
474                         data["token"] = self.token
475
476                 # Check for username and password.
477                 elif self.username and self.password:
478                         data.update({
479                                 "username" : self.username,
480                                 "password" : self.password,
481                         })
482
483                 # Raise an error if no auth details are given.
484                 else:
485                         raise DDNSConfigurationError
486
487                 # Send update to the server.
488                 response = self.send_request(self.url, data=data)
489
490                 # Handle success messages.
491                 if response.code == 200:
492                         return
493
494                 # Handle error codes.
495                 if response.code == 403:
496                         raise DDNSAuthenticationError
497                 elif response.code == 400:
498                         raise DDNSRequestError
499
500                 # If we got here, some other update error happened.
501                 raise DDNSUpdateError
502
503
504 class DDNSProviderNOIP(DDNSProviderDynDNS):
505         INFO = {
506                 "handle"    : "no-ip.com",
507                 "name"      : "No-IP",
508                 "website"   : "http://www.no-ip.com/",
509                 "protocols" : ["ipv4",]
510         }
511
512         # Information about the format of the HTTP request is to be found
513         # here: http://www.no-ip.com/integrate/request and
514         # here: http://www.no-ip.com/integrate/response
515
516         url = "http://dynupdate.no-ip.com/nic/update"
517
518         def _prepare_request_data(self):
519                 data = {
520                         "hostname" : self.hostname,
521                         "address"  : self.get_address("ipv4"),
522                 }
523
524                 return data
525
526
527 class DDNSProviderOVH(DDNSProviderDynDNS):
528         INFO = {
529                 "handle"    : "ovh.com",
530                 "name"      : "OVH",
531                 "website"   : "http://www.ovh.com/",
532                 "protocols" : ["ipv4",]
533         }
534
535         # OVH only provides very limited information about how to
536         # update a DynDNS host. They only provide the update url
537         # on the their german subpage.
538         #
539         # http://hilfe.ovh.de/DomainDynHost
540
541         url = "https://www.ovh.com/nic/update"
542
543         def _prepare_request_data(self):
544                 data = {
545                         "hostname" : self.hostname,
546                         "myip"     : self.get_address("ipv4"),
547                         "system"   : "dyndns",
548                 }
549
550
551 class DDNSProviderRegfish(DDNSProvider):
552         INFO = {
553                 "handle"    : "regfish.com",
554                 "name"      : "Regfish GmbH",
555                 "website"   : "http://www.regfish.com/",
556                 "protocols" : ["ipv6", "ipv4",]
557         }
558
559         # A full documentation to the providers api can be found here
560         # but is only available in german.
561         # https://www.regfish.de/domains/dyndns/dokumentation
562
563         url = "https://dyndns.regfish.de/"
564
565         def update(self):
566                 data = {
567                         "fqdn" : self.hostname,
568                 }
569
570                 # Check if we update an IPv6 address.
571                 address6 = self.get_address("ipv6")
572                 if address6:
573                         data["ipv6"] = address6
574
575                 # Check if we update an IPv4 address.
576                 address4 = self.get_address("ipv4")
577                 if address4:
578                         data["ipv4"] = address4
579
580                 # Raise an error if none address is given.
581                 if not data.has_key("ipv6") and not data.has_key("ipv4"):
582                         raise DDNSConfigurationError
583
584                 # Check if a token has been set.
585                 if self.token:
586                         data["token"] = self.token
587
588                 # Raise an error if no token and no useranem and password
589                 # are given.
590                 elif not self.username and not self.password:
591                         raise DDNSConfigurationError(_("No Auth details specified."))
592
593                 # HTTP Basic Auth is only allowed if no token is used.
594                 if self.token:
595                         # Send update to the server.
596                         response = self.send_request(self.url, data=data)
597                 else:
598                         # Send update to the server.
599                         response = self.send_request(self.url, username=self.username, password=self.password,
600                                 data=data)
601
602                 # Get the full response message.
603                 output = response.read()
604
605                 # Handle success messages.
606                 if "100" in output or "101" in output:
607                         return
608
609                 # Handle error codes.
610                 if "401" or "402" in output:
611                         raise DDNSAuthenticationError
612                 elif "408" in output:
613                         raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
614                 elif "409" in output:
615                         raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
616                 elif "412" in output:
617                         raise DDNSRequestError(_("No valid FQDN was given."))
618                 elif "414" in output:
619                         raise DDNSInternalServerError
620
621                 # If we got here, some other update error happened.
622                 raise DDNSUpdateError
623
624
625 class DDNSProviderSelfhost(DDNSProvider):
626         INFO = {
627                 "handle"    : "selfhost.de",
628                 "name"      : "Selfhost.de",
629                 "website"   : "http://www.selfhost.de/",
630                 "protocols" : ["ipv4",],
631         }
632
633         url = "https://carol.selfhost.de/update"
634
635         def update(self):
636                 data = {
637                         "username" : self.username,
638                         "password" : self.password,
639                         "textmodi" : "1",
640                 }
641
642                 response = self.send_request(self.url, data=data)
643
644                 match = re.search("status=20(0|4)", response.read())
645                 if not match:
646                         raise DDNSUpdateError
647
648
649 class DDNSProviderSPDNS(DDNSProviderDynDNS):
650         INFO = {
651                 "handle"    : "spdns.org",
652                 "name"      : "SPDNS",
653                 "website"   : "http://spdns.org/",
654                 "protocols" : ["ipv4",]
655         }
656
657         # Detailed information about request and response codes are provided
658         # by the vendor. They are using almost the same mechanism and status
659         # codes as dyndns.org so we can inherit all those stuff.
660         #
661         # http://wiki.securepoint.de/index.php/SPDNS_FAQ
662         # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
663
664         url = "https://update.spdns.de/nic/update"
665
666
667 class DDNSProviderVariomedia(DDNSProviderDynDNS):
668         INFO = {
669                 "handle"   : "variomedia.de",
670                 "name"     : "Variomedia",
671                 "website"  : "http://www.variomedia.de/",
672                 "protocols" : ["ipv6", "ipv4",]
673         }
674
675         # Detailed information about the request can be found here
676         # https://dyndns.variomedia.de/
677
678         url = "https://dyndns.variomedia.de/nic/update"
679
680         @property
681         def proto(self):
682                 return self.get("proto")
683
684         def _prepare_request_data(self):
685                 data = {
686                         "hostname" : self.hostname,
687                         "myip"     : self.get_address(self.proto)
688                 }