Rebuild no-ip to inherit all required actions from dyndns.
[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                 data = {
177                         "domain"       : self.hostname,
178                         "ip"           : self.get_address("ipv4"),
179                         "hostcmd"      : "edit",
180                         "hostcmdstage" : "2",
181                         "type"         : "4",
182                 }
183
184                 # Send update to the server.
185                 response = self.send_request(self.url, username=self.username, password=self.password,
186                         data=data)
187
188                 # Handle success messages.
189                 if response.code == 200:
190                         return
191
192                 # Handle error codes.
193                 elif response.code == 401:
194                         raise DDNSAuthenticationError
195
196                 # If we got here, some other update error happened.
197                 raise DDNSUpdateError
198
199
200 class DDNSProviderDNSpark(DDNSProvider):
201         INFO = {
202                 "handle"    : "dnspark.com",
203                 "name"      : "DNS Park",
204                 "website"   : "http://dnspark.com/",
205                 "protocols" : ["ipv4",]
206         }
207
208         # Informations to the used api can be found here:
209         # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
210         url = "https://control.dnspark.com/api/dynamic/update.php"
211
212         def update(self):
213                 data = {
214                         "domain" : self.hostname,
215                         "ip"     : self.get_address("ipv4"),
216                 }
217
218                 # Send update to the server.
219                 response = self.send_request(self.url, username=self.username, password=self.password,
220                         data=data)
221
222                 # Get the full response message.
223                 output = response.read()
224
225                 # Handle success messages.
226                 if output.startswith("ok") or output.startswith("nochange"):
227                         return
228
229                 # Handle error codes.
230                 if output == "unauth":
231                         raise DDNSAuthenticationError
232                 elif output == "abuse":
233                         raise DDNSAbuseError
234                 elif output == "blocked":
235                         raise DDNSBlockedError
236                 elif output == "nofqdn":
237                         raise DDNSRequestError(_("No valid FQDN was given."))
238                 elif output == "nohost":
239                         raise DDNSRequestError(_("Invalid hostname specified."))
240                 elif output == "notdyn":
241                         raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
242                 elif output == "invalid":
243                         raise DDNSRequestError(_("Invalid IP address has been sent."))
244
245                 # If we got here, some other update error happened.
246                 raise DDNSUpdateError
247
248
249 class DDNSProviderDtDNS(DDNSProvider):
250         INFO = {
251                 "handle"    : "dtdns.com",
252                 "name"      : "DtDNS",
253                 "website"   : "http://dtdns.com/",
254                 "protocols" : ["ipv4",]
255                 }
256
257         # Information about the format of the HTTPS request is to be found
258         # http://www.dtdns.com/dtsite/updatespec
259         url = "https://www.dtdns.com/api/autodns.cfm"
260
261
262         def update(self):
263                 data = {
264                         "ip" : self.get_address("ipv4"),
265                         "id" : self.hostname,
266                         "pw" : self.password
267                 }
268
269                 # Send update to the server.
270                 response = self.send_request(self.url, data=data)
271
272                 # Get the full response message.
273                 output = response.read()
274
275                 # Remove all leading and trailing whitespace.
276                 output = output.strip()
277
278                 # Handle success messages.
279                 if "now points to" in output:
280                         return
281
282                 # Handle error codes.
283                 if output == "No hostname to update was supplied.":
284                         raise DDNSRequestError(_("No hostname specified."))
285
286                 elif output == "The hostname you supplied is not valid.":
287                         raise DDNSRequestError(_("Invalid hostname specified."))
288
289                 elif output == "The password you supplied is not valid.":
290                         raise DDNSAuthenticationError
291
292                 elif output == "Administration has disabled this account.":
293                         raise DDNSRequestError(_("Account has been disabled."))
294
295                 elif output == "Illegal character in IP.":
296                         raise DDNSRequestError(_("Invalid IP address has been sent."))
297
298                 elif output == "Too many failed requests.":
299                         raise DDNSRequestError(_("Too many failed requests."))
300
301                 # If we got here, some other update error happened.
302                 raise DDNSUpdateError
303
304
305 class DDNSProviderDynDNS(DDNSProvider):
306         INFO = {
307                 "handle"    : "dyndns.org",
308                 "name"      : "Dyn",
309                 "website"   : "http://dyn.com/dns/",
310                 "protocols" : ["ipv4",]
311         }
312
313         # Information about the format of the request is to be found
314         # http://http://dyn.com/support/developers/api/perform-update/
315         # http://dyn.com/support/developers/api/return-codes/
316         url = "https://members.dyndns.org/nic/update"
317
318         def _prepare_request_data(self):
319                 data = {
320                         "hostname" : self.hostname,
321                         "myip"     : self.get_address("ipv4"),
322                 }
323
324                 return data
325
326         def update(self):
327                 data = self._prepare_request_data()
328
329                 # Send update to the server.
330                 response = self.send_request(self.url, data=data,
331                         username=self.username, password=self.password)
332
333                 # Get the full response message.
334                 output = response.read()
335
336                 # Handle success messages.
337                 if output.startswith("good") or output.startswith("nochg"):
338                         return
339
340                 # Handle error codes.
341                 if output == "badauth":
342                         raise DDNSAuthenticationError
343                 elif output == "aduse":
344                         raise DDNSAbuseError
345                 elif output == "notfqdn":
346                         raise DDNSRequestError(_("No valid FQDN was given."))
347                 elif output == "nohost":
348                         raise DDNSRequestError(_("Specified host does not exist."))
349                 elif output == "911":
350                         raise DDNSInternalServerError
351                 elif output == "dnserr":
352                         raise DDNSInternalServerError(_("DNS error encountered."))
353
354                 # If we got here, some other update error happened.
355                 raise DDNSUpdateError
356
357
358 class DDNSProviderLightningWireLabs(DDNSProvider):
359         INFO = {
360                 "handle"    : "dns.lightningwirelabs.com",
361                 "name"      : "Lightning Wire Labs",
362                 "website"   : "http://dns.lightningwirelabs.com/",
363                 "protocols" : ["ipv6", "ipv4",]
364         }
365
366         # Information about the format of the HTTPS request is to be found
367         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
368         url = "https://dns.lightningwirelabs.com/update"
369
370         @property
371         def token(self):
372                 """
373                         Fast access to the token.
374                 """
375                 return self.get("token")
376
377         def update(self):
378                 data =  {
379                         "hostname" : self.hostname,
380                 }
381
382                 # Check if we update an IPv6 address.
383                 address6 = self.get_address("ipv6")
384                 if address6:
385                         data["address6"] = address6
386
387                 # Check if we update an IPv4 address.
388                 address4 = self.get_address("ipv4")
389                 if address4:
390                         data["address4"] = address4
391
392                 # Raise an error if none address is given.
393                 if not data.has_key("address6") and not data.has_key("address4"):
394                         raise DDNSConfigurationError
395
396                 # Check if a token has been set.
397                 if self.token:
398                         data["token"] = self.token
399
400                 # Check for username and password.
401                 elif self.username and self.password:
402                         data.update({
403                                 "username" : self.username,
404                                 "password" : self.password,
405                         })
406
407                 # Raise an error if no auth details are given.
408                 else:
409                         raise DDNSConfigurationError
410
411                 # Send update to the server.
412                 response = self.send_request(self.url, data=data)
413
414                 # Handle success messages.
415                 if response.code == 200:
416                         return
417
418                 # Handle error codes.
419                 if response.code == 403:
420                         raise DDNSAuthenticationError
421                 elif response.code == 400:
422                         raise DDNSRequestError
423
424                 # If we got here, some other update error happened.
425                 raise DDNSUpdateError
426
427
428 class DDNSProviderNOIP(DDNSProviderDynDNS):
429         INFO = {
430                 "handle"    : "no-ip.com",
431                 "name"      : "No-IP",
432                 "website"   : "http://www.no-ip.com/",
433                 "protocols" : ["ipv4",]
434         }
435
436         # Information about the format of the HTTP request is to be found
437         # here: http://www.no-ip.com/integrate/request and
438         # here: http://www.no-ip.com/integrate/response
439
440         url = "http://dynupdate.no-ip.com/nic/update"
441
442         def _prepare_request_data(self):
443                 data = {
444                         "hostname" : self.hostname,
445                         "address"  : self.get_address("ipv4"),
446                 }
447
448                 return data
449
450
451 class DDNSProviderSelfhost(DDNSProvider):
452         INFO = {
453                 "handle"    : "selfhost.de",
454                 "name"      : "Selfhost.de",
455                 "website"   : "http://www.selfhost.de/",
456                 "protocols" : ["ipv4",],
457         }
458
459         url = "https://carol.selfhost.de/update"
460
461         def update(self):
462                 data = {
463                         "username" : self.username,
464                         "password" : self.password,
465                         "textmodi" : "1",
466                 }
467
468                 response = self.send_request(self.url, data=data)
469
470                 match = re.search("status=20(0|4)", response.read())
471                 if not match:
472                         raise DDNSUpdateError