Add freedns.afraid.org as new provider.
[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
9da3e685
MT
118 def __call__(self, force=False):
119 if force:
120 logger.info(_("Updating %s forced") % self.hostname)
121
7399fc5b 122 # Check if we actually need to update this host.
9da3e685 123 elif self.is_uptodate(self.protocols):
7399fc5b
MT
124 logger.info(_("%s is already up to date") % self.hostname)
125 return
126
127 # Execute the update.
5f402f36
MT
128 self.update()
129
130 def update(self):
f22ab085
MT
131 raise NotImplementedError
132
7399fc5b
MT
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
f22ab085
MT
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
f3cf1f70
SS
163class 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
5f402f36 175 def update(self):
f3cf1f70
SS
176 url = self.url % {
177 "username" : self.username,
178 "password" : self.password,
179 }
180
181 data = {
182 "domain" : self.hostname,
183 "ip" : self.get_address("ipv4"),
184 "hostcmd" : "edit",
185 "hostcmdstage" : "2",
186 "type" : "4",
187 }
188
189 # Send update to the server.
190 response = self.send_request(url, username=self.username, password=self.password,
191 data=data)
192
193 # Handle success messages.
194 if response.code == 200:
195 return
196
197 # Handle error codes.
4caed6ed 198 elif response.code == 401:
f3cf1f70
SS
199 raise DDNSAuthenticationError
200
201 # If we got here, some other update error happened.
202 raise DDNSUpdateError
203
204
39301272
SS
205class DDNSProviderDNSpark(DDNSProvider):
206 INFO = {
207 "handle" : "dnspark.com",
208 "name" : "DNS Park",
209 "website" : "http://dnspark.com/",
210 "protocols" : ["ipv4",]
211 }
212
213 # Informations to the used api can be found here:
214 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
215 url = "https://control.dnspark.com/api/dynamic/update.php"
216
5f402f36 217 def update(self):
39301272
SS
218 url = self.url % {
219 "username" : self.username,
220 "password" : self.password,
221 }
222
223 data = {
224 "domain" : self.hostname,
225 "ip" : self.get_address("ipv4"),
226 }
227
228 # Send update to the server.
229 response = self.send_request(url, username=self.username, password=self.password,
230 data=data)
231
232 # Get the full response message.
233 output = response.read()
234
235 # Handle success messages.
236 if output.startswith("ok") or output.startswith("nochange"):
237 return
238
239 # Handle error codes.
240 if output == "unauth":
241 raise DDNSAuthenticationError
242 elif output == "abuse":
243 raise DDNSAbuseError
244 elif output == "blocked":
245 raise DDNSBlockedError
246 elif output == "nofqdn":
247 raise DDNSRequestError(_("No valid FQDN was given."))
248 elif output == "nohost":
249 raise DDNSRequestError(_("Invalid hostname specified."))
250 elif output == "notdyn":
251 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
252 elif output == "invalid":
253 raise DDNSRequestError(_("Invalid IP address has been sent."))
254
255 # If we got here, some other update error happened.
256 raise DDNSUpdateError
257
43b2cd59
SS
258
259class DDNSProviderDtDNS(DDNSProvider):
260 INFO = {
261 "handle" : "dtdns.com",
262 "name" : "DtDNS",
263 "website" : "http://dtdns.com/",
264 "protocols" : ["ipv4",]
265 }
266
267 # Information about the format of the HTTPS request is to be found
268 # http://www.dtdns.com/dtsite/updatespec
269 url = "https://www.dtdns.com/api/autodns.cfm"
270
271
272 def update(self):
273 data = {
274 "ip" : self.get_address("ipv4"),
275 "id" : self.hostname,
276 "pw" : self.password
277 }
278
279 # Send update to the server.
280 response = self.send_request(self.url, data=data)
281
282 # Get the full response message.
283 output = response.read()
284
285 # Remove all leading and trailing whitespace.
286 output = output.strip()
287
288 # Handle success messages.
289 if "now points to" in output:
290 return
291
292 # Handle error codes.
293 if output == "No hostname to update was supplied.":
294 raise DDNSRequestError(_("No hostname specified."))
295
296 elif output == "The hostname you supplied is not valid.":
297 raise DDNSRequestError(_("Invalid hostname specified."))
298
299 elif output == "The password you supplied is not valid.":
300 raise DDNSAuthenticationError
301
302 elif output == "Administration has disabled this account.":
303 raise DDNSRequestError(_("Account has been disabled."))
304
305 elif output == "Illegal character in IP.":
306 raise DDNSRequestError(_("Invalid IP address has been sent."))
307
308 elif output == "Too many failed requests.":
309 raise DDNSRequestError(_("Too many failed requests."))
310
311 # If we got here, some other update error happened.
312 raise DDNSUpdateError
313
314
aa21a4c6
SS
315class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
316 INFO = {
317 "handle" : "freedns.afraid.org",
318 "name" : "freedns.afraid.org",
319 "website" : "http://freedns.afraid.org/",
320 "protocols" : ["ipv6", "ipv4",]
321 }
322
323 # No information about the request or response could be found on the vendor
324 # page. All used values have been collected by testing.
325 url = "https://freedns.afraid.org/dynamic/update.php"
326
327 @property
328 def proto(self):
329 return self.get("proto")
330
331 def update(self):
332 address = self.get_address(self.proto)
333
334 data = {
335 "address" : address,
336 }
337
338 # Add auth token to the update url.
339 url = "%s?%s" % (self.url, self.token)
340
341 # Send update to the server.
342 response = self.send_request(url, data=data)
343
344 # Get the full response message.
345 output = response.read()
346
347 # Handle success messages.
348 if output.startswith("Updated") or "has not changed" in output:
349 return
350
351 # Handle error codes.
352 if output == "ERROR: Unable to locate this record":
353 raise DDNSAuthenticationError
354 elif "is an invalid IP address" in output:
355 raise DDNSRequestError(_("Invalid IP address has been sent."))
356
357 # If we got here, some other update error happened.
358 raise DDNSUpdateError
359
360
a08c1b72
SS
361class DDNSProviderLightningWireLabs(DDNSProvider):
362 INFO = {
363 "handle" : "dns.lightningwirelabs.com",
364 "name" : "Lightning Wire Labs",
365 "website" : "http://dns.lightningwirelabs.com/",
366 "protocols" : ["ipv6", "ipv4",]
367 }
368
369 # Information about the format of the HTTPS request is to be found
370 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
371 url = "https://dns.lightningwirelabs.com/update"
372
373 @property
374 def token(self):
375 """
376 Fast access to the token.
377 """
378 return self.get("token")
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