Various providers: properly inherit from DynDNS class.
[oddments/ddns.git] / src / ddns / providers.py
CommitLineData
f22ab085 1#!/usr/bin/python
3fdcb9d1
MT
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###############################################################################
f22ab085 21
7399fc5b
MT
22import logging
23
24from i18n import _
25
f22ab085
MT
26# Import all possible exception types.
27from .errors import *
28
7399fc5b
MT
29logger = logging.getLogger("ddns.providers")
30logger.propagate = 1
31
f22ab085
MT
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
7399fc5b
MT
114 @property
115 def protocols(self):
116 return self.INFO.get("protocols")
117
46687828
SS
118 @property
119 def token(self):
120 """
121 Fast access to the token.
122 """
123 return self.get("token")
124
9da3e685
MT
125 def __call__(self, force=False):
126 if force:
127 logger.info(_("Updating %s forced") % self.hostname)
128
7399fc5b 129 # Check if we actually need to update this host.
9da3e685 130 elif self.is_uptodate(self.protocols):
7399fc5b
MT
131 logger.info(_("%s is already up to date") % self.hostname)
132 return
133
134 # Execute the update.
5f402f36
MT
135 self.update()
136
137 def update(self):
f22ab085
MT
138 raise NotImplementedError
139
7399fc5b
MT
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
f22ab085
MT
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
f3cf1f70
SS
170class 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
5f402f36 182 def update(self):
f3cf1f70
SS
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.
175c9b80 192 response = self.send_request(self.url, username=self.username, password=self.password,
f3cf1f70
SS
193 data=data)
194
195 # Handle success messages.
196 if response.code == 200:
197 return
198
199 # Handle error codes.
4caed6ed 200 elif response.code == 401:
f3cf1f70
SS
201 raise DDNSAuthenticationError
202
203 # If we got here, some other update error happened.
204 raise DDNSUpdateError
205
206
39301272
SS
207class 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
5f402f36 219 def update(self):
39301272
SS
220 data = {
221 "domain" : self.hostname,
222 "ip" : self.get_address("ipv4"),
223 }
224
225 # Send update to the server.
175c9b80 226 response = self.send_request(self.url, username=self.username, password=self.password,
39301272
SS
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
43b2cd59
SS
255
256class 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
43b2cd59
SS
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
bfed6701
SS
311class 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
88f39629 324 def _prepare_request_data(self):
bfed6701
SS
325 data = {
326 "hostname" : self.hostname,
327 "myip" : self.get_address("ipv4"),
328 }
329
88f39629
SS
330 return data
331
332 def update(self):
333 data = self._prepare_request_data()
334
bfed6701 335 # Send update to the server.
88f39629
SS
336 response = self.send_request(self.url, data=data,
337 username=self.username, password=self.password)
bfed6701
SS
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
3a8407fa
SS
364class 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):
54d3efc8
MT
380 data = DDNSProviderDynDNS._prepare_request_data(self)
381
382 # This one supports IPv6
383 data.update({
3a8407fa 384 "myipv6" : self.get_address("ipv6"),
54d3efc8
MT
385 })
386
387 return data
3a8407fa
SS
388
389
ee071271
SS
390class DDNSProviderEasyDNS(DDNSProviderDynDNS):
391 INFO = {
392 "handle" : "easydns.com",
393 "name" : "EasyDNS",
394 "website" : "http://www.easydns.com/",
395 "protocols" : ["ipv4",]
396 }
397
398 # There is only some basic documentation provided by the vendor,
399 # also searching the web gain very poor results.
400 # http://mediawiki.easydns.com/index.php/Dynamic_DNS
401
402 url = "http://api.cp.easydns.com/dyn/tomato.php"
403
404
aa21a4c6
SS
405class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
406 INFO = {
407 "handle" : "freedns.afraid.org",
408 "name" : "freedns.afraid.org",
409 "website" : "http://freedns.afraid.org/",
410 "protocols" : ["ipv6", "ipv4",]
411 }
412
413 # No information about the request or response could be found on the vendor
414 # page. All used values have been collected by testing.
415 url = "https://freedns.afraid.org/dynamic/update.php"
416
417 @property
418 def proto(self):
419 return self.get("proto")
420
421 def update(self):
422 address = self.get_address(self.proto)
423
424 data = {
425 "address" : address,
426 }
427
428 # Add auth token to the update url.
429 url = "%s?%s" % (self.url, self.token)
430
431 # Send update to the server.
432 response = self.send_request(url, data=data)
433
aa21a4c6
SS
434 if output.startswith("Updated") or "has not changed" in output:
435 return
436
437 # Handle error codes.
438 if output == "ERROR: Unable to locate this record":
439 raise DDNSAuthenticationError
440 elif "is an invalid IP address" in output:
441 raise DDNSRequestError(_("Invalid IP address has been sent."))
442
aa21a4c6 443
a08c1b72
SS
444class DDNSProviderLightningWireLabs(DDNSProvider):
445 INFO = {
446 "handle" : "dns.lightningwirelabs.com",
447 "name" : "Lightning Wire Labs",
448 "website" : "http://dns.lightningwirelabs.com/",
449 "protocols" : ["ipv6", "ipv4",]
450 }
451
452 # Information about the format of the HTTPS request is to be found
453 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
454 url = "https://dns.lightningwirelabs.com/update"
455
5f402f36 456 def update(self):
a08c1b72
SS
457 data = {
458 "hostname" : self.hostname,
459 }
460
461 # Check if we update an IPv6 address.
462 address6 = self.get_address("ipv6")
463 if address6:
464 data["address6"] = address6
465
466 # Check if we update an IPv4 address.
467 address4 = self.get_address("ipv4")
468 if address4:
469 data["address4"] = address4
470
471 # Raise an error if none address is given.
472 if not data.has_key("address6") and not data.has_key("address4"):
473 raise DDNSConfigurationError
474
475 # Check if a token has been set.
476 if self.token:
477 data["token"] = self.token
478
479 # Check for username and password.
480 elif self.username and self.password:
481 data.update({
482 "username" : self.username,
483 "password" : self.password,
484 })
485
486 # Raise an error if no auth details are given.
487 else:
488 raise DDNSConfigurationError
489
490 # Send update to the server.
cb455540 491 response = self.send_request(self.url, data=data)
a08c1b72
SS
492
493 # Handle success messages.
494 if response.code == 200:
495 return
496
497 # Handle error codes.
2e5ad318 498 if response.code == 403:
a08c1b72 499 raise DDNSAuthenticationError
2e5ad318 500 elif response.code == 400:
a08c1b72
SS
501 raise DDNSRequestError
502
503 # If we got here, some other update error happened.
504 raise DDNSUpdateError
505
506
88f39629 507class DDNSProviderNOIP(DDNSProviderDynDNS):
f22ab085
MT
508 INFO = {
509 "handle" : "no-ip.com",
510 "name" : "No-IP",
511 "website" : "http://www.no-ip.com/",
512 "protocols" : ["ipv4",]
513 }
514
515 # Information about the format of the HTTP request is to be found
516 # here: http://www.no-ip.com/integrate/request and
517 # here: http://www.no-ip.com/integrate/response
518
88f39629 519 url = "http://dynupdate.no-ip.com/nic/update"
2de06f59 520
88f39629 521 def _prepare_request_data(self):
2de06f59
MT
522 data = {
523 "hostname" : self.hostname,
f22ab085
MT
524 "address" : self.get_address("ipv4"),
525 }
526
88f39629 527 return data
f22ab085
MT
528
529
a508bda6
SS
530class DDNSProviderOVH(DDNSProviderDynDNS):
531 INFO = {
532 "handle" : "ovh.com",
533 "name" : "OVH",
534 "website" : "http://www.ovh.com/",
535 "protocols" : ["ipv4",]
536 }
537
538 # OVH only provides very limited information about how to
539 # update a DynDNS host. They only provide the update url
540 # on the their german subpage.
541 #
542 # http://hilfe.ovh.de/DomainDynHost
543
544 url = "https://www.ovh.com/nic/update"
545
546 def _prepare_request_data(self):
54d3efc8
MT
547 data = DDNSProviderDynDNS._prepare_request_data(self)
548 data.update({
549 "system" : "dyndns",
550 })
551
552 return data
a508bda6
SS
553
554
ef33455e
SS
555class DDNSProviderRegfish(DDNSProvider):
556 INFO = {
557 "handle" : "regfish.com",
558 "name" : "Regfish GmbH",
559 "website" : "http://www.regfish.com/",
560 "protocols" : ["ipv6", "ipv4",]
561 }
562
563 # A full documentation to the providers api can be found here
564 # but is only available in german.
565 # https://www.regfish.de/domains/dyndns/dokumentation
566
567 url = "https://dyndns.regfish.de/"
568
569 def update(self):
570 data = {
571 "fqdn" : self.hostname,
572 }
573
574 # Check if we update an IPv6 address.
575 address6 = self.get_address("ipv6")
576 if address6:
577 data["ipv6"] = address6
578
579 # Check if we update an IPv4 address.
580 address4 = self.get_address("ipv4")
581 if address4:
582 data["ipv4"] = address4
583
584 # Raise an error if none address is given.
585 if not data.has_key("ipv6") and not data.has_key("ipv4"):
586 raise DDNSConfigurationError
587
588 # Check if a token has been set.
589 if self.token:
590 data["token"] = self.token
591
592 # Raise an error if no token and no useranem and password
593 # are given.
594 elif not self.username and not self.password:
595 raise DDNSConfigurationError(_("No Auth details specified."))
596
597 # HTTP Basic Auth is only allowed if no token is used.
598 if self.token:
599 # Send update to the server.
600 response = self.send_request(self.url, data=data)
601 else:
602 # Send update to the server.
603 response = self.send_request(self.url, username=self.username, password=self.password,
604 data=data)
605
606 # Get the full response message.
607 output = response.read()
608
609 # Handle success messages.
610 if "100" in output or "101" in output:
611 return
612
613 # Handle error codes.
614 if "401" or "402" in output:
615 raise DDNSAuthenticationError
616 elif "408" in output:
617 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
618 elif "409" in output:
619 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
620 elif "412" in output:
621 raise DDNSRequestError(_("No valid FQDN was given."))
622 elif "414" in output:
623 raise DDNSInternalServerError
624
625 # If we got here, some other update error happened.
626 raise DDNSUpdateError
627
628
f22ab085
MT
629class DDNSProviderSelfhost(DDNSProvider):
630 INFO = {
631 "handle" : "selfhost.de",
632 "name" : "Selfhost.de",
633 "website" : "http://www.selfhost.de/",
634 "protocols" : ["ipv4",],
635 }
636
2de06f59 637 url = "https://carol.selfhost.de/update"
f22ab085 638
5f402f36 639 def update(self):
2de06f59
MT
640 data = {
641 "username" : self.username,
642 "password" : self.password,
643 "textmodi" : "1",
644 }
f22ab085 645
2de06f59 646 response = self.send_request(self.url, data=data)
f22ab085
MT
647
648 match = re.search("status=20(0|4)", response.read())
649 if not match:
650 raise DDNSUpdateError
b09b1545
SS
651
652
653class DDNSProviderSPDNS(DDNSProviderDynDNS):
654 INFO = {
655 "handle" : "spdns.org",
656 "name" : "SPDNS",
657 "website" : "http://spdns.org/",
658 "protocols" : ["ipv4",]
659 }
660
661 # Detailed information about request and response codes are provided
662 # by the vendor. They are using almost the same mechanism and status
663 # codes as dyndns.org so we can inherit all those stuff.
664 #
665 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
666 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
667
668 url = "https://update.spdns.de/nic/update"
4ec90b93
MT
669
670
c8c7ca8f
SS
671class DDNSProviderVariomedia(DDNSProviderDynDNS):
672 INFO = {
673 "handle" : "variomedia.de",
674 "name" : "Variomedia",
675 "website" : "http://www.variomedia.de/",
676 "protocols" : ["ipv6", "ipv4",]
677 }
678
679 # Detailed information about the request can be found here
680 # https://dyndns.variomedia.de/
681
682 url = "https://dyndns.variomedia.de/nic/update"
683
684 @property
685 def proto(self):
686 return self.get("proto")
687
688 def _prepare_request_data(self):
689 data = {
690 "hostname" : self.hostname,
691 "myip" : self.get_address(self.proto)
692 }
54d3efc8
MT
693
694 return data