Add DtDNS as new provider.
[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                 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
205 class 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
259 class 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
315 class DDNSProviderLightningWireLabs(DDNSProvider):
316         INFO = {
317                 "handle"    : "dns.lightningwirelabs.com",
318                 "name"      : "Lightning Wire Labs",
319                 "website"   : "http://dns.lightningwirelabs.com/",
320                 "protocols" : ["ipv6", "ipv4",]
321         }
322
323         # Information about the format of the HTTPS request is to be found
324         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
325         url = "https://dns.lightningwirelabs.com/update"
326
327         @property
328         def token(self):
329                 """
330                         Fast access to the token.
331                 """
332                 return self.get("token")
333
334         def update(self):
335                 data =  {
336                         "hostname" : self.hostname,
337                 }
338
339                 # Check if we update an IPv6 address.
340                 address6 = self.get_address("ipv6")
341                 if address6:
342                         data["address6"] = address6
343
344                 # Check if we update an IPv4 address.
345                 address4 = self.get_address("ipv4")
346                 if address4:
347                         data["address4"] = address4
348
349                 # Raise an error if none address is given.
350                 if not data.has_key("address6") and not data.has_key("address4"):
351                         raise DDNSConfigurationError
352
353                 # Check if a token has been set.
354                 if self.token:
355                         data["token"] = self.token
356
357                 # Check for username and password.
358                 elif self.username and self.password:
359                         data.update({
360                                 "username" : self.username,
361                                 "password" : self.password,
362                         })
363
364                 # Raise an error if no auth details are given.
365                 else:
366                         raise DDNSConfigurationError
367
368                 # Send update to the server.
369                 response = self.send_request(self.url, data=data)
370
371                 # Handle success messages.
372                 if response.code == 200:
373                         return
374
375                 # Handle error codes.
376                 if response.code == 403:
377                         raise DDNSAuthenticationError
378                 elif response.code == 400:
379                         raise DDNSRequestError
380
381                 # If we got here, some other update error happened.
382                 raise DDNSUpdateError
383
384
385 class DDNSProviderNOIP(DDNSProvider):
386         INFO = {
387                 "handle"    : "no-ip.com",
388                 "name"      : "No-IP",
389                 "website"   : "http://www.no-ip.com/",
390                 "protocols" : ["ipv4",]
391         }
392
393         # Information about the format of the HTTP request is to be found
394         # here: http://www.no-ip.com/integrate/request and
395         # here: http://www.no-ip.com/integrate/response
396
397         url = "http://%(username)s:%(password)s@dynupdate.no-ip.com/nic/update"
398
399         def update(self):
400                 url = self.url % {
401                         "username" : self.username,
402                         "password" : self.password,
403                 }
404
405                 data = {
406                         "hostname" : self.hostname,
407                         "address"  : self.get_address("ipv4"),
408                 }
409
410                 # Send update to the server.
411                 response = self.send_request(url, data=data)
412
413                 # Get the full response message.
414                 output = response.read()
415
416                 # Handle success messages.
417                 if output.startswith("good") or output.startswith("nochg"):
418                         return
419
420                 # Handle error codes.
421                 if output == "badauth":
422                         raise DDNSAuthenticationError
423                 elif output == "aduse":
424                         raise DDNSAbuseError
425                 elif output == "911":
426                         raise DDNSInternalServerError
427
428                 # If we got here, some other update error happened.
429                 raise DDNSUpdateError
430
431
432 class DDNSProviderSelfhost(DDNSProvider):
433         INFO = {
434                 "handle"    : "selfhost.de",
435                 "name"      : "Selfhost.de",
436                 "website"   : "http://www.selfhost.de/",
437                 "protocols" : ["ipv4",],
438         }
439
440         url = "https://carol.selfhost.de/update"
441
442         def update(self):
443                 data = {
444                         "username" : self.username,
445                         "password" : self.password,
446                         "textmodi" : "1",
447                 }
448
449                 response = self.send_request(self.url, data=data)
450
451                 match = re.search("status=20(0|4)", response.read())
452                 if not match:
453                         raise DDNSUpdateError