]> git.ipfire.org Git - oddments/ddns.git/blame_incremental - src/ddns/providers.py
Allow forcing an update.
[oddments/ddns.git] / src / ddns / providers.py
... / ...
CommitLineData
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
22import logging
23
24from i18n import _
25
26# Import all possible exception types.
27from .errors import *
28
29logger = logging.getLogger("ddns.providers")
30logger.propagate = 1
31
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
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
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
175 def update(self):
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.
198 elif response.code == "401":
199 raise DDNSAuthenticationError
200
201 # If we got here, some other update error happened.
202 raise DDNSUpdateError
203
204
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
217 def update(self):
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
258
259class DDNSProviderLightningWireLabs(DDNSProvider):
260 INFO = {
261 "handle" : "dns.lightningwirelabs.com",
262 "name" : "Lightning Wire Labs",
263 "website" : "http://dns.lightningwirelabs.com/",
264 "protocols" : ["ipv6", "ipv4",]
265 }
266
267 # Information about the format of the HTTPS request is to be found
268 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
269 url = "https://dns.lightningwirelabs.com/update"
270
271 @property
272 def token(self):
273 """
274 Fast access to the token.
275 """
276 return self.get("token")
277
278 def update(self):
279 data = {
280 "hostname" : self.hostname,
281 }
282
283 # Check if we update an IPv6 address.
284 address6 = self.get_address("ipv6")
285 if address6:
286 data["address6"] = address6
287
288 # Check if we update an IPv4 address.
289 address4 = self.get_address("ipv4")
290 if address4:
291 data["address4"] = address4
292
293 # Raise an error if none address is given.
294 if not data.has_key("address6") and not data.has_key("address4"):
295 raise DDNSConfigurationError
296
297 # Check if a token has been set.
298 if self.token:
299 data["token"] = self.token
300
301 # Check for username and password.
302 elif self.username and self.password:
303 data.update({
304 "username" : self.username,
305 "password" : self.password,
306 })
307
308 # Raise an error if no auth details are given.
309 else:
310 raise DDNSConfigurationError
311
312 # Send update to the server.
313 response = self.send_request(self.url, data=data)
314
315 # Handle success messages.
316 if response.code == 200:
317 return
318
319 # Handle error codes.
320 if response.code == 403:
321 raise DDNSAuthenticationError
322 elif response.code == 400:
323 raise DDNSRequestError
324
325 # If we got here, some other update error happened.
326 raise DDNSUpdateError
327
328
329class DDNSProviderNOIP(DDNSProvider):
330 INFO = {
331 "handle" : "no-ip.com",
332 "name" : "No-IP",
333 "website" : "http://www.no-ip.com/",
334 "protocols" : ["ipv4",]
335 }
336
337 # Information about the format of the HTTP request is to be found
338 # here: http://www.no-ip.com/integrate/request and
339 # here: http://www.no-ip.com/integrate/response
340
341 url = "http://%(username)s:%(password)s@dynupdate.no-ip.com/nic/update"
342
343 def update(self):
344 url = self.url % {
345 "username" : self.username,
346 "password" : self.password,
347 }
348
349 data = {
350 "hostname" : self.hostname,
351 "address" : self.get_address("ipv4"),
352 }
353
354 # Send update to the server.
355 response = self.send_request(url, data=data)
356
357 # Get the full response message.
358 output = response.read()
359
360 # Handle success messages.
361 if output.startswith("good") or output.startswith("nochg"):
362 return
363
364 # Handle error codes.
365 if output == "badauth":
366 raise DDNSAuthenticationError
367 elif output == "aduse":
368 raise DDNSAbuseError
369 elif output == "911":
370 raise DDNSInternalServerError
371
372 # If we got here, some other update error happened.
373 raise DDNSUpdateError
374
375
376class DDNSProviderSelfhost(DDNSProvider):
377 INFO = {
378 "handle" : "selfhost.de",
379 "name" : "Selfhost.de",
380 "website" : "http://www.selfhost.de/",
381 "protocols" : ["ipv4",],
382 }
383
384 url = "https://carol.selfhost.de/update"
385
386 def update(self):
387 data = {
388 "username" : self.username,
389 "password" : self.password,
390 "textmodi" : "1",
391 }
392
393 response = self.send_request(self.url, data=data)
394
395 match = re.search("status=20(0|4)", response.read())
396 if not match:
397 raise DDNSUpdateError