Move call to get token to main class.
[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
MT
22import logging
23
24from i18n import _
25
f22ab085
MT
26# Import all possible exception types.
27from .errors import *
28
7399fc5b
MT
29logger = logging.getLogger("ddns.providers")
30logger.propagate = 1
31
f22ab085
MT
32class 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
7399fc5b
MT
114 @property
115 def protocols(self):
116 return self.INFO.get("protocols")
117
46687828
SS
118 @property
119 def token(self):
120 """
121 Fast access to the token.
122 """
123 return self.get("token")
124
9da3e685
MT
125 def __call__(self, force=False):
126 if force:
127 logger.info(_("Updating %s forced") % self.hostname)
128
7399fc5b 129 # Check if we actually need to update this host.
9da3e685 130 elif self.is_uptodate(self.protocols):
7399fc5b
MT
131 logger.info(_("%s is already up to date") % self.hostname)
132 return
133
134 # Execute the update.
5f402f36
MT
135 self.update()
136
137 def update(self):
f22ab085
MT
138 raise NotImplementedError
139
7399fc5b
MT
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
f22ab085
MT
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
f3cf1f70
SS
170class 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
5f402f36 182 def update(self):
f3cf1f70
SS
183 url = self.url % {
184 "username" : self.username,
185 "password" : self.password,
186 }
187
188 data = {
189 "domain" : self.hostname,
190 "ip" : self.get_address("ipv4"),
191 "hostcmd" : "edit",
192 "hostcmdstage" : "2",
193 "type" : "4",
194 }
195
196 # Send update to the server.
197 response = self.send_request(url, username=self.username, password=self.password,
198 data=data)
199
200 # Handle success messages.
201 if response.code == 200:
202 return
203
204 # Handle error codes.
4caed6ed 205 elif response.code == 401:
f3cf1f70
SS
206 raise DDNSAuthenticationError
207
208 # If we got here, some other update error happened.
209 raise DDNSUpdateError
210
211
39301272
SS
212class DDNSProviderDNSpark(DDNSProvider):
213 INFO = {
214 "handle" : "dnspark.com",
215 "name" : "DNS Park",
216 "website" : "http://dnspark.com/",
217 "protocols" : ["ipv4",]
218 }
219
220 # Informations to the used api can be found here:
221 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
222 url = "https://control.dnspark.com/api/dynamic/update.php"
223
5f402f36 224 def update(self):
39301272
SS
225 url = self.url % {
226 "username" : self.username,
227 "password" : self.password,
228 }
229
230 data = {
231 "domain" : self.hostname,
232 "ip" : self.get_address("ipv4"),
233 }
234
235 # Send update to the server.
236 response = self.send_request(url, username=self.username, password=self.password,
237 data=data)
238
239 # Get the full response message.
240 output = response.read()
241
242 # Handle success messages.
243 if output.startswith("ok") or output.startswith("nochange"):
244 return
245
246 # Handle error codes.
247 if output == "unauth":
248 raise DDNSAuthenticationError
249 elif output == "abuse":
250 raise DDNSAbuseError
251 elif output == "blocked":
252 raise DDNSBlockedError
253 elif output == "nofqdn":
254 raise DDNSRequestError(_("No valid FQDN was given."))
255 elif output == "nohost":
256 raise DDNSRequestError(_("Invalid hostname specified."))
257 elif output == "notdyn":
258 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
259 elif output == "invalid":
260 raise DDNSRequestError(_("Invalid IP address has been sent."))
261
262 # If we got here, some other update error happened.
263 raise DDNSUpdateError
264
43b2cd59
SS
265
266class DDNSProviderDtDNS(DDNSProvider):
267 INFO = {
268 "handle" : "dtdns.com",
269 "name" : "DtDNS",
270 "website" : "http://dtdns.com/",
271 "protocols" : ["ipv4",]
272 }
273
274 # Information about the format of the HTTPS request is to be found
275 # http://www.dtdns.com/dtsite/updatespec
276 url = "https://www.dtdns.com/api/autodns.cfm"
277
278
279 def update(self):
280 data = {
281 "ip" : self.get_address("ipv4"),
282 "id" : self.hostname,
283 "pw" : self.password
284 }
285
286 # Send update to the server.
287 response = self.send_request(self.url, data=data)
288
289 # Get the full response message.
290 output = response.read()
291
292 # Remove all leading and trailing whitespace.
293 output = output.strip()
294
295 # Handle success messages.
296 if "now points to" in output:
297 return
298
299 # Handle error codes.
300 if output == "No hostname to update was supplied.":
301 raise DDNSRequestError(_("No hostname specified."))
302
303 elif output == "The hostname you supplied is not valid.":
304 raise DDNSRequestError(_("Invalid hostname specified."))
305
306 elif output == "The password you supplied is not valid.":
307 raise DDNSAuthenticationError
308
309 elif output == "Administration has disabled this account.":
310 raise DDNSRequestError(_("Account has been disabled."))
311
312 elif output == "Illegal character in IP.":
313 raise DDNSRequestError(_("Invalid IP address has been sent."))
314
315 elif output == "Too many failed requests.":
316 raise DDNSRequestError(_("Too many failed requests."))
317
318 # If we got here, some other update error happened.
319 raise DDNSUpdateError
320
321
aa21a4c6
SS
322class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
323 INFO = {
324 "handle" : "freedns.afraid.org",
325 "name" : "freedns.afraid.org",
326 "website" : "http://freedns.afraid.org/",
327 "protocols" : ["ipv6", "ipv4",]
328 }
329
330 # No information about the request or response could be found on the vendor
331 # page. All used values have been collected by testing.
332 url = "https://freedns.afraid.org/dynamic/update.php"
333
334 @property
335 def proto(self):
336 return self.get("proto")
337
338 def update(self):
339 address = self.get_address(self.proto)
340
341 data = {
342 "address" : address,
343 }
344
345 # Add auth token to the update url.
346 url = "%s?%s" % (self.url, self.token)
347
348 # Send update to the server.
349 response = self.send_request(url, data=data)
350
351 # Get the full response message.
352 output = response.read()
353
354 # Handle success messages.
355 if output.startswith("Updated") or "has not changed" in output:
356 return
357
358 # Handle error codes.
359 if output == "ERROR: Unable to locate this record":
360 raise DDNSAuthenticationError
361 elif "is an invalid IP address" in output:
362 raise DDNSRequestError(_("Invalid IP address has been sent."))
363
364 # If we got here, some other update error happened.
365 raise DDNSUpdateError
366
367
a08c1b72
SS
368class DDNSProviderLightningWireLabs(DDNSProvider):
369 INFO = {
370 "handle" : "dns.lightningwirelabs.com",
371 "name" : "Lightning Wire Labs",
372 "website" : "http://dns.lightningwirelabs.com/",
373 "protocols" : ["ipv6", "ipv4",]
374 }
375
376 # Information about the format of the HTTPS request is to be found
377 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
378 url = "https://dns.lightningwirelabs.com/update"
379
5f402f36 380 def update(self):
a08c1b72
SS
381 data = {
382 "hostname" : self.hostname,
383 }
384
385 # Check if we update an IPv6 address.
386 address6 = self.get_address("ipv6")
387 if address6:
388 data["address6"] = address6
389
390 # Check if we update an IPv4 address.
391 address4 = self.get_address("ipv4")
392 if address4:
393 data["address4"] = address4
394
395 # Raise an error if none address is given.
396 if not data.has_key("address6") and not data.has_key("address4"):
397 raise DDNSConfigurationError
398
399 # Check if a token has been set.
400 if self.token:
401 data["token"] = self.token
402
403 # Check for username and password.
404 elif self.username and self.password:
405 data.update({
406 "username" : self.username,
407 "password" : self.password,
408 })
409
410 # Raise an error if no auth details are given.
411 else:
412 raise DDNSConfigurationError
413
414 # Send update to the server.
cb455540 415 response = self.send_request(self.url, data=data)
a08c1b72
SS
416
417 # Handle success messages.
418 if response.code == 200:
419 return
420
421 # Handle error codes.
2e5ad318 422 if response.code == 403:
a08c1b72 423 raise DDNSAuthenticationError
2e5ad318 424 elif response.code == 400:
a08c1b72
SS
425 raise DDNSRequestError
426
427 # If we got here, some other update error happened.
428 raise DDNSUpdateError
429
430
f22ab085
MT
431class DDNSProviderNOIP(DDNSProvider):
432 INFO = {
433 "handle" : "no-ip.com",
434 "name" : "No-IP",
435 "website" : "http://www.no-ip.com/",
436 "protocols" : ["ipv4",]
437 }
438
439 # Information about the format of the HTTP request is to be found
440 # here: http://www.no-ip.com/integrate/request and
441 # here: http://www.no-ip.com/integrate/response
442
2de06f59 443 url = "http://%(username)s:%(password)s@dynupdate.no-ip.com/nic/update"
f22ab085 444
5f402f36 445 def update(self):
f22ab085 446 url = self.url % {
f22ab085
MT
447 "username" : self.username,
448 "password" : self.password,
2de06f59
MT
449 }
450
451 data = {
452 "hostname" : self.hostname,
f22ab085
MT
453 "address" : self.get_address("ipv4"),
454 }
455
456 # Send update to the server.
2de06f59 457 response = self.send_request(url, data=data)
f22ab085
MT
458
459 # Get the full response message.
460 output = response.read()
461
462 # Handle success messages.
463 if output.startswith("good") or output.startswith("nochg"):
464 return
465
466 # Handle error codes.
467 if output == "badauth":
468 raise DDNSAuthenticationError
469 elif output == "aduse":
470 raise DDNSAbuseError
471 elif output == "911":
472 raise DDNSInternalServerError
473
474 # If we got here, some other update error happened.
475 raise DDNSUpdateError
476
477
478class DDNSProviderSelfhost(DDNSProvider):
479 INFO = {
480 "handle" : "selfhost.de",
481 "name" : "Selfhost.de",
482 "website" : "http://www.selfhost.de/",
483 "protocols" : ["ipv4",],
484 }
485
2de06f59 486 url = "https://carol.selfhost.de/update"
f22ab085 487
5f402f36 488 def update(self):
2de06f59
MT
489 data = {
490 "username" : self.username,
491 "password" : self.password,
492 "textmodi" : "1",
493 }
f22ab085 494
2de06f59 495 response = self.send_request(self.url, data=data)
f22ab085
MT
496
497 match = re.search("status=20(0|4)", response.read())
498 if not match:
499 raise DDNSUpdateError