]> git.ipfire.org Git - oddments/ddns.git/blob - src/ddns/providers.py
Merge remote-tracking branch 'stevee/dynu.com'
[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 @property
119 def token(self):
120 """
121 Fast access to the token.
122 """
123 return self.get("token")
124
125 def __call__(self, force=False):
126 if force:
127 logger.info(_("Updating %s forced") % self.hostname)
128
129 # Check if we actually need to update this host.
130 elif self.is_uptodate(self.protocols):
131 logger.info(_("%s is already up to date") % self.hostname)
132 return
133
134 # Execute the update.
135 self.update()
136
137 def update(self):
138 raise NotImplementedError
139
140 def is_uptodate(self, protos):
141 """
142 Returns True if this host is already up to date
143 and does not need to change the IP address on the
144 name server.
145 """
146 for proto in protos:
147 addresses = self.core.system.resolve(self.hostname, proto)
148
149 current_address = self.get_address(proto)
150
151 if not current_address in addresses:
152 return False
153
154 return True
155
156 def send_request(self, *args, **kwargs):
157 """
158 Proxy connection to the send request
159 method.
160 """
161 return self.core.system.send_request(*args, **kwargs)
162
163 def get_address(self, proto):
164 """
165 Proxy method to get the current IP address.
166 """
167 return self.core.system.get_address(proto)
168
169
170 class DDNSProviderDHS(DDNSProvider):
171 INFO = {
172 "handle" : "dhs.org",
173 "name" : "DHS International",
174 "website" : "http://dhs.org/",
175 "protocols" : ["ipv4",]
176 }
177
178 # No information about the used update api provided on webpage,
179 # grabed from source code of ez-ipudate.
180 url = "http://members.dhs.org/nic/hosts"
181
182 def update(self):
183 data = {
184 "domain" : self.hostname,
185 "ip" : self.get_address("ipv4"),
186 "hostcmd" : "edit",
187 "hostcmdstage" : "2",
188 "type" : "4",
189 }
190
191 # Send update to the server.
192 response = self.send_request(self.url, username=self.username, password=self.password,
193 data=data)
194
195 # Handle success messages.
196 if response.code == 200:
197 return
198
199 # Handle error codes.
200 elif response.code == 401:
201 raise DDNSAuthenticationError
202
203 # If we got here, some other update error happened.
204 raise DDNSUpdateError
205
206
207 class DDNSProviderDNSpark(DDNSProvider):
208 INFO = {
209 "handle" : "dnspark.com",
210 "name" : "DNS Park",
211 "website" : "http://dnspark.com/",
212 "protocols" : ["ipv4",]
213 }
214
215 # Informations to the used api can be found here:
216 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
217 url = "https://control.dnspark.com/api/dynamic/update.php"
218
219 def update(self):
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(self.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 DDNSProviderDtDNS(DDNSProvider):
257 INFO = {
258 "handle" : "dtdns.com",
259 "name" : "DtDNS",
260 "website" : "http://dtdns.com/",
261 "protocols" : ["ipv4",]
262 }
263
264 # Information about the format of the HTTPS request is to be found
265 # http://www.dtdns.com/dtsite/updatespec
266 url = "https://www.dtdns.com/api/autodns.cfm"
267
268 def update(self):
269 data = {
270 "ip" : self.get_address("ipv4"),
271 "id" : self.hostname,
272 "pw" : self.password
273 }
274
275 # Send update to the server.
276 response = self.send_request(self.url, data=data)
277
278 # Get the full response message.
279 output = response.read()
280
281 # Remove all leading and trailing whitespace.
282 output = output.strip()
283
284 # Handle success messages.
285 if "now points to" in output:
286 return
287
288 # Handle error codes.
289 if output == "No hostname to update was supplied.":
290 raise DDNSRequestError(_("No hostname specified."))
291
292 elif output == "The hostname you supplied is not valid.":
293 raise DDNSRequestError(_("Invalid hostname specified."))
294
295 elif output == "The password you supplied is not valid.":
296 raise DDNSAuthenticationError
297
298 elif output == "Administration has disabled this account.":
299 raise DDNSRequestError(_("Account has been disabled."))
300
301 elif output == "Illegal character in IP.":
302 raise DDNSRequestError(_("Invalid IP address has been sent."))
303
304 elif output == "Too many failed requests.":
305 raise DDNSRequestError(_("Too many failed requests."))
306
307 # If we got here, some other update error happened.
308 raise DDNSUpdateError
309
310
311 class DDNSProviderDynDNS(DDNSProvider):
312 INFO = {
313 "handle" : "dyndns.org",
314 "name" : "Dyn",
315 "website" : "http://dyn.com/dns/",
316 "protocols" : ["ipv4",]
317 }
318
319 # Information about the format of the request is to be found
320 # http://http://dyn.com/support/developers/api/perform-update/
321 # http://dyn.com/support/developers/api/return-codes/
322 url = "https://members.dyndns.org/nic/update"
323
324 def _prepare_request_data(self):
325 data = {
326 "hostname" : self.hostname,
327 "myip" : self.get_address("ipv4"),
328 }
329
330 return data
331
332 def update(self):
333 data = self._prepare_request_data()
334
335 # Send update to the server.
336 response = self.send_request(self.url, data=data,
337 username=self.username, password=self.password)
338
339 # Get the full response message.
340 output = response.read()
341
342 # Handle success messages.
343 if output.startswith("good") or output.startswith("nochg"):
344 return
345
346 # Handle error codes.
347 if output == "badauth":
348 raise DDNSAuthenticationError
349 elif output == "aduse":
350 raise DDNSAbuseError
351 elif output == "notfqdn":
352 raise DDNSRequestError(_("No valid FQDN was given."))
353 elif output == "nohost":
354 raise DDNSRequestError(_("Specified host does not exist."))
355 elif output == "911":
356 raise DDNSInternalServerError
357 elif output == "dnserr":
358 raise DDNSInternalServerError(_("DNS error encountered."))
359
360 # If we got here, some other update error happened.
361 raise DDNSUpdateError
362
363
364 class DDNSProviderDynU(DDNSProviderDynDNS):
365 INFO = {
366 "handle" : "dynu.com",
367 "name" : "Dynu",
368 "website" : "http://dynu.com/",
369 "protocols" : ["ipv6", "ipv4",]
370 }
371
372
373 # Detailed information about the request and response codes
374 # are available on the providers webpage.
375 # http://dynu.com/Default.aspx?page=dnsapi
376
377 url = "https://api.dynu.com/nic/update"
378
379 def _prepare_request_data(self):
380 data = {
381 "hostname" : self.hostname,
382 "myip" : self.get_address("ipv4"),
383 "myipv6" : self.get_address("ipv6"),
384 }
385
386
387 class DDNSProviderEasyDNS(DDNSProviderDynDNS):
388 INFO = {
389 "handle" : "easydns.com",
390 "name" : "EasyDNS",
391 "website" : "http://www.easydns.com/",
392 "protocols" : ["ipv4",]
393 }
394
395 # There is only some basic documentation provided by the vendor,
396 # also searching the web gain very poor results.
397 # http://mediawiki.easydns.com/index.php/Dynamic_DNS
398
399 url = "http://api.cp.easydns.com/dyn/tomato.php"
400
401
402 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
403 INFO = {
404 "handle" : "freedns.afraid.org",
405 "name" : "freedns.afraid.org",
406 "website" : "http://freedns.afraid.org/",
407 "protocols" : ["ipv6", "ipv4",]
408 }
409
410 # No information about the request or response could be found on the vendor
411 # page. All used values have been collected by testing.
412 url = "https://freedns.afraid.org/dynamic/update.php"
413
414 @property
415 def proto(self):
416 return self.get("proto")
417
418 def update(self):
419 address = self.get_address(self.proto)
420
421 data = {
422 "address" : address,
423 }
424
425 # Add auth token to the update url.
426 url = "%s?%s" % (self.url, self.token)
427
428 # Send update to the server.
429 response = self.send_request(url, data=data)
430
431 if output.startswith("Updated") or "has not changed" in output:
432 return
433
434 # Handle error codes.
435 if output == "ERROR: Unable to locate this record":
436 raise DDNSAuthenticationError
437 elif "is an invalid IP address" in output:
438 raise DDNSRequestError(_("Invalid IP address has been sent."))
439
440
441 class DDNSProviderLightningWireLabs(DDNSProvider):
442 INFO = {
443 "handle" : "dns.lightningwirelabs.com",
444 "name" : "Lightning Wire Labs",
445 "website" : "http://dns.lightningwirelabs.com/",
446 "protocols" : ["ipv6", "ipv4",]
447 }
448
449 # Information about the format of the HTTPS request is to be found
450 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
451 url = "https://dns.lightningwirelabs.com/update"
452
453 def update(self):
454 data = {
455 "hostname" : self.hostname,
456 }
457
458 # Check if we update an IPv6 address.
459 address6 = self.get_address("ipv6")
460 if address6:
461 data["address6"] = address6
462
463 # Check if we update an IPv4 address.
464 address4 = self.get_address("ipv4")
465 if address4:
466 data["address4"] = address4
467
468 # Raise an error if none address is given.
469 if not data.has_key("address6") and not data.has_key("address4"):
470 raise DDNSConfigurationError
471
472 # Check if a token has been set.
473 if self.token:
474 data["token"] = self.token
475
476 # Check for username and password.
477 elif self.username and self.password:
478 data.update({
479 "username" : self.username,
480 "password" : self.password,
481 })
482
483 # Raise an error if no auth details are given.
484 else:
485 raise DDNSConfigurationError
486
487 # Send update to the server.
488 response = self.send_request(self.url, data=data)
489
490 # Handle success messages.
491 if response.code == 200:
492 return
493
494 # Handle error codes.
495 if response.code == 403:
496 raise DDNSAuthenticationError
497 elif response.code == 400:
498 raise DDNSRequestError
499
500 # If we got here, some other update error happened.
501 raise DDNSUpdateError
502
503
504 class DDNSProviderNOIP(DDNSProviderDynDNS):
505 INFO = {
506 "handle" : "no-ip.com",
507 "name" : "No-IP",
508 "website" : "http://www.no-ip.com/",
509 "protocols" : ["ipv4",]
510 }
511
512 # Information about the format of the HTTP request is to be found
513 # here: http://www.no-ip.com/integrate/request and
514 # here: http://www.no-ip.com/integrate/response
515
516 url = "http://dynupdate.no-ip.com/nic/update"
517
518 def _prepare_request_data(self):
519 data = {
520 "hostname" : self.hostname,
521 "address" : self.get_address("ipv4"),
522 }
523
524 return data
525
526
527 class DDNSProviderOVH(DDNSProviderDynDNS):
528 INFO = {
529 "handle" : "ovh.com",
530 "name" : "OVH",
531 "website" : "http://www.ovh.com/",
532 "protocols" : ["ipv4",]
533 }
534
535 # OVH only provides very limited information about how to
536 # update a DynDNS host. They only provide the update url
537 # on the their german subpage.
538 #
539 # http://hilfe.ovh.de/DomainDynHost
540
541 url = "https://www.ovh.com/nic/update"
542
543 def _prepare_request_data(self):
544 data = {
545 "hostname" : self.hostname,
546 "myip" : self.get_address("ipv4"),
547 "system" : "dyndns",
548 }
549
550
551 class DDNSProviderRegfish(DDNSProvider):
552 INFO = {
553 "handle" : "regfish.com",
554 "name" : "Regfish GmbH",
555 "website" : "http://www.regfish.com/",
556 "protocols" : ["ipv6", "ipv4",]
557 }
558
559 # A full documentation to the providers api can be found here
560 # but is only available in german.
561 # https://www.regfish.de/domains/dyndns/dokumentation
562
563 url = "https://dyndns.regfish.de/"
564
565 def update(self):
566 data = {
567 "fqdn" : self.hostname,
568 }
569
570 # Check if we update an IPv6 address.
571 address6 = self.get_address("ipv6")
572 if address6:
573 data["ipv6"] = address6
574
575 # Check if we update an IPv4 address.
576 address4 = self.get_address("ipv4")
577 if address4:
578 data["ipv4"] = address4
579
580 # Raise an error if none address is given.
581 if not data.has_key("ipv6") and not data.has_key("ipv4"):
582 raise DDNSConfigurationError
583
584 # Check if a token has been set.
585 if self.token:
586 data["token"] = self.token
587
588 # Raise an error if no token and no useranem and password
589 # are given.
590 elif not self.username and not self.password:
591 raise DDNSConfigurationError(_("No Auth details specified."))
592
593 # HTTP Basic Auth is only allowed if no token is used.
594 if self.token:
595 # Send update to the server.
596 response = self.send_request(self.url, data=data)
597 else:
598 # Send update to the server.
599 response = self.send_request(self.url, username=self.username, password=self.password,
600 data=data)
601
602 # Get the full response message.
603 output = response.read()
604
605 # Handle success messages.
606 if "100" in output or "101" in output:
607 return
608
609 # Handle error codes.
610 if "401" or "402" in output:
611 raise DDNSAuthenticationError
612 elif "408" in output:
613 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
614 elif "409" in output:
615 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
616 elif "412" in output:
617 raise DDNSRequestError(_("No valid FQDN was given."))
618 elif "414" in output:
619 raise DDNSInternalServerError
620
621 # If we got here, some other update error happened.
622 raise DDNSUpdateError
623
624
625 class DDNSProviderSelfhost(DDNSProvider):
626 INFO = {
627 "handle" : "selfhost.de",
628 "name" : "Selfhost.de",
629 "website" : "http://www.selfhost.de/",
630 "protocols" : ["ipv4",],
631 }
632
633 url = "https://carol.selfhost.de/update"
634
635 def update(self):
636 data = {
637 "username" : self.username,
638 "password" : self.password,
639 "textmodi" : "1",
640 }
641
642 response = self.send_request(self.url, data=data)
643
644 match = re.search("status=20(0|4)", response.read())
645 if not match:
646 raise DDNSUpdateError
647
648
649 class DDNSProviderSPDNS(DDNSProviderDynDNS):
650 INFO = {
651 "handle" : "spdns.org",
652 "name" : "SPDNS",
653 "website" : "http://spdns.org/",
654 "protocols" : ["ipv4",]
655 }
656
657 # Detailed information about request and response codes are provided
658 # by the vendor. They are using almost the same mechanism and status
659 # codes as dyndns.org so we can inherit all those stuff.
660 #
661 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
662 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
663
664 url = "https://update.spdns.de/nic/update"
665
666
667 class DDNSProviderVariomedia(DDNSProviderDynDNS):
668 INFO = {
669 "handle" : "variomedia.de",
670 "name" : "Variomedia",
671 "website" : "http://www.variomedia.de/",
672 "protocols" : ["ipv6", "ipv4",]
673 }
674
675 # Detailed information about the request can be found here
676 # https://dyndns.variomedia.de/
677
678 url = "https://dyndns.variomedia.de/nic/update"
679
680 @property
681 def proto(self):
682 return self.get("proto")
683
684 def _prepare_request_data(self):
685 data = {
686 "hostname" : self.hostname,
687 "myip" : self.get_address(self.proto)
688 }