8a5185fe7aaf42352a4932270765f0642881312c
[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         def __call__(self, force=False):
119                 if force:
120                         logger.info(_("Updating %s forced") % self.hostname)
121
122                 # Check if we actually need to update this host.
123                 elif self.is_uptodate(self.protocols):
124                         logger.info(_("%s is already up to date") % self.hostname)
125                         return
126
127                 # Execute the update.
128                 self.update()
129
130         def update(self):
131                 raise NotImplementedError
132
133         def is_uptodate(self, protos):
134                 """
135                         Returns True if this host is already up to date
136                         and does not need to change the IP address on the
137                         name server.
138                 """
139                 for proto in protos:
140                         addresses = self.core.system.resolve(self.hostname, proto)
141
142                         current_address = self.get_address(proto)
143
144                         if not current_address in addresses:
145                                 return False
146
147                 return True
148
149         def send_request(self, *args, **kwargs):
150                 """
151                         Proxy connection to the send request
152                         method.
153                 """
154                 return self.core.system.send_request(*args, **kwargs)
155
156         def get_address(self, proto):
157                 """
158                         Proxy method to get the current IP address.
159                 """
160                 return self.core.system.get_address(proto)
161
162
163 class DDNSProviderDHS(DDNSProvider):
164         INFO = {
165                 "handle"    : "dhs.org",
166                 "name"      : "DHS International",
167                 "website"   : "http://dhs.org/",
168                 "protocols" : ["ipv4",]
169         }
170
171         # No information about the used update api provided on webpage,
172         # grabed from source code of ez-ipudate.
173         url = "http://members.dhs.org/nic/hosts"
174
175         def update(self):
176                 data = {
177                         "domain"       : self.hostname,
178                         "ip"           : self.get_address("ipv4"),
179                         "hostcmd"      : "edit",
180                         "hostcmdstage" : "2",
181                         "type"         : "4",
182                 }
183
184                 # Send update to the server.
185                 response = self.send_request(self.url, username=self.username, password=self.password,
186                         data=data)
187
188                 # Handle success messages.
189                 if response.code == 200:
190                         return
191
192                 # Handle error codes.
193                 elif response.code == 401:
194                         raise DDNSAuthenticationError
195
196                 # If we got here, some other update error happened.
197                 raise DDNSUpdateError
198
199
200 class DDNSProviderDNSpark(DDNSProvider):
201         INFO = {
202                 "handle"    : "dnspark.com",
203                 "name"      : "DNS Park",
204                 "website"   : "http://dnspark.com/",
205                 "protocols" : ["ipv4",]
206         }
207
208         # Informations to the used api can be found here:
209         # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
210         url = "https://control.dnspark.com/api/dynamic/update.php"
211
212         def update(self):
213                 data = {
214                         "domain" : self.hostname,
215                         "ip"     : self.get_address("ipv4"),
216                 }
217
218                 # Send update to the server.
219                 response = self.send_request(self.url, username=self.username, password=self.password,
220                         data=data)
221
222                 # Get the full response message.
223                 output = response.read()
224
225                 # Handle success messages.
226                 if output.startswith("ok") or output.startswith("nochange"):
227                         return
228
229                 # Handle error codes.
230                 if output == "unauth":
231                         raise DDNSAuthenticationError
232                 elif output == "abuse":
233                         raise DDNSAbuseError
234                 elif output == "blocked":
235                         raise DDNSBlockedError
236                 elif output == "nofqdn":
237                         raise DDNSRequestError(_("No valid FQDN was given."))
238                 elif output == "nohost":
239                         raise DDNSRequestError(_("Invalid hostname specified."))
240                 elif output == "notdyn":
241                         raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
242                 elif output == "invalid":
243                         raise DDNSRequestError(_("Invalid IP address has been sent."))
244
245                 # If we got here, some other update error happened.
246                 raise DDNSUpdateError
247
248
249 class DDNSProviderDtDNS(DDNSProvider):
250         INFO = {
251                 "handle"    : "dtdns.com",
252                 "name"      : "DtDNS",
253                 "website"   : "http://dtdns.com/",
254                 "protocols" : ["ipv4",]
255                 }
256
257         # Information about the format of the HTTPS request is to be found
258         # http://www.dtdns.com/dtsite/updatespec
259         url = "https://www.dtdns.com/api/autodns.cfm"
260
261
262         def update(self):
263                 data = {
264                         "ip" : self.get_address("ipv4"),
265                         "id" : self.hostname,
266                         "pw" : self.password
267                 }
268
269                 # Send update to the server.
270                 response = self.send_request(self.url, data=data)
271
272                 # Get the full response message.
273                 output = response.read()
274
275                 # Remove all leading and trailing whitespace.
276                 output = output.strip()
277
278                 # Handle success messages.
279                 if "now points to" in output:
280                         return
281
282                 # Handle error codes.
283                 if output == "No hostname to update was supplied.":
284                         raise DDNSRequestError(_("No hostname specified."))
285
286                 elif output == "The hostname you supplied is not valid.":
287                         raise DDNSRequestError(_("Invalid hostname specified."))
288
289                 elif output == "The password you supplied is not valid.":
290                         raise DDNSAuthenticationError
291
292                 elif output == "Administration has disabled this account.":
293                         raise DDNSRequestError(_("Account has been disabled."))
294
295                 elif output == "Illegal character in IP.":
296                         raise DDNSRequestError(_("Invalid IP address has been sent."))
297
298                 elif output == "Too many failed requests.":
299                         raise DDNSRequestError(_("Too many failed requests."))
300
301                 # If we got here, some other update error happened.
302                 raise DDNSUpdateError
303
304
305 class DDNSProviderDynDNS(DDNSProvider):
306         INFO = {
307                 "handle"    : "dyndns.org",
308                 "name"      : "Dyn",
309                 "website"   : "http://dyn.com/dns/",
310                 "protocols" : ["ipv4",]
311         }
312
313         # Information about the format of the request is to be found
314         # http://http://dyn.com/support/developers/api/perform-update/
315         # http://dyn.com/support/developers/api/return-codes/
316         url = "https://members.dyndns.org/nic/update"
317
318         def update(self):
319                 data = {
320                         "hostname" : self.hostname,
321                         "myip"     : self.get_address("ipv4"),
322                 }
323
324                 # Send update to the server.
325                 response = self.send_request(self.url, username=self.username, password=self.password,
326                         data=data)
327
328                 # Get the full response message.
329                 output = response.read()
330
331                 # Handle success messages.
332                 if output.startswith("good") or output.startswith("nochg"):
333                         return
334
335                 # Handle error codes.
336                 if output == "badauth":
337                         raise DDNSAuthenticationError
338                 elif output == "aduse":
339                         raise DDNSAbuseError
340                 elif output == "notfqdn":
341                         raise DDNSRequestError(_("No valid FQDN was given."))
342                 elif output == "nohost":
343                         raise DDNSRequestError(_("Specified host does not exist."))
344                 elif output == "911":
345                         raise DDNSInternalServerError
346                 elif output == "dnserr":
347                         raise DDNSInternalServerError(_("DNS error encountered."))
348
349                 # If we got here, some other update error happened.
350                 raise DDNSUpdateError
351
352
353 class DDNSProviderLightningWireLabs(DDNSProvider):
354         INFO = {
355                 "handle"    : "dns.lightningwirelabs.com",
356                 "name"      : "Lightning Wire Labs",
357                 "website"   : "http://dns.lightningwirelabs.com/",
358                 "protocols" : ["ipv6", "ipv4",]
359         }
360
361         # Information about the format of the HTTPS request is to be found
362         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
363         url = "https://dns.lightningwirelabs.com/update"
364
365         @property
366         def token(self):
367                 """
368                         Fast access to the token.
369                 """
370                 return self.get("token")
371
372         def update(self):
373                 data =  {
374                         "hostname" : self.hostname,
375                 }
376
377                 # Check if we update an IPv6 address.
378                 address6 = self.get_address("ipv6")
379                 if address6:
380                         data["address6"] = address6
381
382                 # Check if we update an IPv4 address.
383                 address4 = self.get_address("ipv4")
384                 if address4:
385                         data["address4"] = address4
386
387                 # Raise an error if none address is given.
388                 if not data.has_key("address6") and not data.has_key("address4"):
389                         raise DDNSConfigurationError
390
391                 # Check if a token has been set.
392                 if self.token:
393                         data["token"] = self.token
394
395                 # Check for username and password.
396                 elif self.username and self.password:
397                         data.update({
398                                 "username" : self.username,
399                                 "password" : self.password,
400                         })
401
402                 # Raise an error if no auth details are given.
403                 else:
404                         raise DDNSConfigurationError
405
406                 # Send update to the server.
407                 response = self.send_request(self.url, data=data)
408
409                 # Handle success messages.
410                 if response.code == 200:
411                         return
412
413                 # Handle error codes.
414                 if response.code == 403:
415                         raise DDNSAuthenticationError
416                 elif response.code == 400:
417                         raise DDNSRequestError
418
419                 # If we got here, some other update error happened.
420                 raise DDNSUpdateError
421
422
423 class DDNSProviderNOIP(DDNSProvider):
424         INFO = {
425                 "handle"    : "no-ip.com",
426                 "name"      : "No-IP",
427                 "website"   : "http://www.no-ip.com/",
428                 "protocols" : ["ipv4",]
429         }
430
431         # Information about the format of the HTTP request is to be found
432         # here: http://www.no-ip.com/integrate/request and
433         # here: http://www.no-ip.com/integrate/response
434
435         url = "http://%(username)s:%(password)s@dynupdate.no-ip.com/nic/update"
436
437         def update(self):
438                 url = self.url % {
439                         "username" : self.username,
440                         "password" : self.password,
441                 }
442
443                 data = {
444                         "hostname" : self.hostname,
445                         "address"  : self.get_address("ipv4"),
446                 }
447
448                 # Send update to the server.
449                 response = self.send_request(url, data=data)
450
451                 # Get the full response message.
452                 output = response.read()
453
454                 # Handle success messages.
455                 if output.startswith("good") or output.startswith("nochg"):
456                         return
457
458                 # Handle error codes.
459                 if output == "badauth":
460                         raise DDNSAuthenticationError
461                 elif output == "aduse":
462                         raise DDNSAbuseError
463                 elif output == "911":
464                         raise DDNSInternalServerError
465
466                 # If we got here, some other update error happened.
467                 raise DDNSUpdateError
468
469
470 class DDNSProviderSelfhost(DDNSProvider):
471         INFO = {
472                 "handle"    : "selfhost.de",
473                 "name"      : "Selfhost.de",
474                 "website"   : "http://www.selfhost.de/",
475                 "protocols" : ["ipv4",],
476         }
477
478         url = "https://carol.selfhost.de/update"
479
480         def update(self):
481                 data = {
482                         "username" : self.username,
483                         "password" : self.password,
484                         "textmodi" : "1",
485                 }
486
487                 response = self.send_request(self.url, data=data)
488
489                 match = re.search("status=20(0|4)", response.read())
490                 if not match:
491                         raise DDNSUpdateError