21159727ad989ca297604ed1574af78901d6568d
[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 def __call__(self):
119 # Check if we actually need to update this host.
120 if self.is_uptodate(self.protocols):
121 logger.info(_("%s is already up to date") % self.hostname)
122 return
123
124 # Execute the update.
125 self.update()
126
127 def update(self):
128 raise NotImplementedError
129
130 def is_uptodate(self, protos):
131 """
132 Returns True if this host is already up to date
133 and does not need to change the IP address on the
134 name server.
135 """
136 for proto in protos:
137 addresses = self.core.system.resolve(self.hostname, proto)
138
139 current_address = self.get_address(proto)
140
141 if not current_address in addresses:
142 return False
143
144 return True
145
146 def send_request(self, *args, **kwargs):
147 """
148 Proxy connection to the send request
149 method.
150 """
151 return self.core.system.send_request(*args, **kwargs)
152
153 def get_address(self, proto):
154 """
155 Proxy method to get the current IP address.
156 """
157 return self.core.system.get_address(proto)
158
159
160 class DDNSProviderDHS(DDNSProvider):
161 INFO = {
162 "handle" : "dhs.org",
163 "name" : "DHS International",
164 "website" : "http://dhs.org/",
165 "protocols" : ["ipv4",]
166 }
167
168 # No information about the used update api provided on webpage,
169 # grabed from source code of ez-ipudate.
170 url = "http://members.dhs.org/nic/hosts"
171
172 def update(self):
173 url = self.url % {
174 "username" : self.username,
175 "password" : self.password,
176 }
177
178 data = {
179 "domain" : self.hostname,
180 "ip" : self.get_address("ipv4"),
181 "hostcmd" : "edit",
182 "hostcmdstage" : "2",
183 "type" : "4",
184 }
185
186 # Send update to the server.
187 response = self.send_request(url, username=self.username, password=self.password,
188 data=data)
189
190 # Handle success messages.
191 if response.code == 200:
192 return
193
194 # Handle error codes.
195 elif response.code == "401":
196 raise DDNSAuthenticationError
197
198 # If we got here, some other update error happened.
199 raise DDNSUpdateError
200
201
202 class DDNSProviderDNSpark(DDNSProvider):
203 INFO = {
204 "handle" : "dnspark.com",
205 "name" : "DNS Park",
206 "website" : "http://dnspark.com/",
207 "protocols" : ["ipv4",]
208 }
209
210 # Informations to the used api can be found here:
211 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
212 url = "https://control.dnspark.com/api/dynamic/update.php"
213
214 def update(self):
215 url = self.url % {
216 "username" : self.username,
217 "password" : self.password,
218 }
219
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(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 DDNSProviderLightningWireLabs(DDNSProvider):
257 INFO = {
258 "handle" : "dns.lightningwirelabs.com",
259 "name" : "Lightning Wire Labs",
260 "website" : "http://dns.lightningwirelabs.com/",
261 "protocols" : ["ipv6", "ipv4",]
262 }
263
264 # Information about the format of the HTTPS request is to be found
265 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
266 url = "https://dns.lightningwirelabs.com/update"
267
268 @property
269 def token(self):
270 """
271 Fast access to the token.
272 """
273 return self.get("token")
274
275 def update(self):
276 data = {
277 "hostname" : self.hostname,
278 }
279
280 # Check if we update an IPv6 address.
281 address6 = self.get_address("ipv6")
282 if address6:
283 data["address6"] = address6
284
285 # Check if we update an IPv4 address.
286 address4 = self.get_address("ipv4")
287 if address4:
288 data["address4"] = address4
289
290 # Raise an error if none address is given.
291 if not data.has_key("address6") and not data.has_key("address4"):
292 raise DDNSConfigurationError
293
294 # Check if a token has been set.
295 if self.token:
296 data["token"] = self.token
297
298 # Check for username and password.
299 elif self.username and self.password:
300 data.update({
301 "username" : self.username,
302 "password" : self.password,
303 })
304
305 # Raise an error if no auth details are given.
306 else:
307 raise DDNSConfigurationError
308
309 # Send update to the server.
310 response = self.send_request(self.url, data=data)
311
312 # Handle success messages.
313 if response.code == 200:
314 return
315
316 # Handle error codes.
317 if response.code == 403:
318 raise DDNSAuthenticationError
319 elif response.code == 400:
320 raise DDNSRequestError
321
322 # If we got here, some other update error happened.
323 raise DDNSUpdateError
324
325
326 class DDNSProviderNOIP(DDNSProvider):
327 INFO = {
328 "handle" : "no-ip.com",
329 "name" : "No-IP",
330 "website" : "http://www.no-ip.com/",
331 "protocols" : ["ipv4",]
332 }
333
334 # Information about the format of the HTTP request is to be found
335 # here: http://www.no-ip.com/integrate/request and
336 # here: http://www.no-ip.com/integrate/response
337
338 url = "http://%(username)s:%(password)s@dynupdate.no-ip.com/nic/update"
339
340 def update(self):
341 url = self.url % {
342 "username" : self.username,
343 "password" : self.password,
344 }
345
346 data = {
347 "hostname" : self.hostname,
348 "address" : self.get_address("ipv4"),
349 }
350
351 # Send update to the server.
352 response = self.send_request(url, data=data)
353
354 # Get the full response message.
355 output = response.read()
356
357 # Handle success messages.
358 if output.startswith("good") or output.startswith("nochg"):
359 return
360
361 # Handle error codes.
362 if output == "badauth":
363 raise DDNSAuthenticationError
364 elif output == "aduse":
365 raise DDNSAbuseError
366 elif output == "911":
367 raise DDNSInternalServerError
368
369 # If we got here, some other update error happened.
370 raise DDNSUpdateError
371
372
373 class DDNSProviderSelfhost(DDNSProvider):
374 INFO = {
375 "handle" : "selfhost.de",
376 "name" : "Selfhost.de",
377 "website" : "http://www.selfhost.de/",
378 "protocols" : ["ipv4",],
379 }
380
381 url = "https://carol.selfhost.de/update"
382
383 def update(self):
384 data = {
385 "username" : self.username,
386 "password" : self.password,
387 "textmodi" : "1",
388 }
389
390 response = self.send_request(self.url, data=data)
391
392 match = re.search("status=20(0|4)", response.read())
393 if not match:
394 raise DDNSUpdateError