]> git.ipfire.org Git - oddments/ddns.git/blame - src/ddns/providers.py
Hold back further updates after failed updates for a while
[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
37e24fbf 22import datetime
7399fc5b 23import logging
64d3fad4 24import os
a892c594 25import subprocess
3b16fdb1 26import urllib2
d1cd57eb 27import xml.dom.minidom
7399fc5b
MT
28
29from i18n import _
30
f22ab085
MT
31# Import all possible exception types.
32from .errors import *
33
7399fc5b
MT
34logger = logging.getLogger("ddns.providers")
35logger.propagate = 1
36
adfe6272
MT
37_providers = {}
38
39def get():
40 """
41 Returns a dict with all automatically registered providers.
42 """
43 return _providers.copy()
44
f22ab085 45class DDNSProvider(object):
6a11646e
MT
46 # A short string that uniquely identifies
47 # this provider.
48 handle = None
f22ab085 49
6a11646e
MT
50 # The full name of the provider.
51 name = None
f22ab085 52
6a11646e
MT
53 # A weburl to the homepage of the provider.
54 # (Where to register a new account?)
55 website = None
f22ab085 56
6a11646e
MT
57 # A list of supported protocols.
58 protocols = ("ipv6", "ipv4")
f22ab085
MT
59
60 DEFAULT_SETTINGS = {}
61
37e24fbf
MT
62 # holdoff time - Number of days no update is performed unless
63 # the IP address has changed.
64 holdoff_days = 30
65
112d3fb8
MT
66 # holdoff time for update failures - Number of days no update
67 # is tried after the last one has failed.
68 holdoff_failure_days = 0.5
69
29c8c9c6
MT
70 # True if the provider is able to remove records, too.
71 # Required to remove AAAA records if IPv6 is absent again.
72 can_remove_records = True
73
adfe6272
MT
74 # Automatically register all providers.
75 class __metaclass__(type):
76 def __init__(provider, name, bases, dict):
77 type.__init__(provider, name, bases, dict)
78
79 # The main class from which is inherited is not registered
80 # as a provider.
81 if name == "DDNSProvider":
82 return
83
84 if not all((provider.handle, provider.name, provider.website)):
85 raise DDNSError(_("Provider is not properly configured"))
86
87 assert not _providers.has_key(provider.handle), \
88 "Provider '%s' has already been registered" % provider.handle
89
90 _providers[provider.handle] = provider
91
64d3fad4
MT
92 @staticmethod
93 def supported():
94 """
95 Should be overwritten to check if the system the code is running
96 on has all the required tools to support this provider.
97 """
98 return True
99
f22ab085
MT
100 def __init__(self, core, **settings):
101 self.core = core
102
103 # Copy a set of default settings and
104 # update them by those from the configuration file.
105 self.settings = self.DEFAULT_SETTINGS.copy()
106 self.settings.update(settings)
107
108 def __repr__(self):
109 return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
110
111 def __cmp__(self, other):
112 return cmp(self.hostname, other.hostname)
113
37e24fbf
MT
114 @property
115 def db(self):
116 return self.core.db
117
f22ab085
MT
118 def get(self, key, default=None):
119 """
120 Get a setting from the settings dictionary.
121 """
122 return self.settings.get(key, default)
123
124 @property
125 def hostname(self):
126 """
127 Fast access to the hostname.
128 """
129 return self.get("hostname")
130
131 @property
132 def username(self):
133 """
134 Fast access to the username.
135 """
136 return self.get("username")
137
138 @property
139 def password(self):
140 """
141 Fast access to the password.
142 """
143 return self.get("password")
144
46687828
SS
145 @property
146 def token(self):
147 """
148 Fast access to the token.
149 """
150 return self.get("token")
151
9da3e685
MT
152 def __call__(self, force=False):
153 if force:
c3888f15 154 logger.debug(_("Updating %s forced") % self.hostname)
9da3e685 155
112d3fb8
MT
156 # Do nothing if the last update has failed or no update is required
157 elif self.has_failure or not self.requires_update:
7399fc5b
MT
158 return
159
160 # Execute the update.
37e24fbf
MT
161 try:
162 self.update()
163
164 # In case of any errors, log the failed request and
165 # raise the exception.
166 except DDNSError as e:
167 self.core.db.log_failure(self.hostname, e)
168 raise
5f402f36 169
12b3818b
MT
170 logger.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \
171 { "hostname" : self.hostname, "provider" : self.name })
37e24fbf 172 self.core.db.log_success(self.hostname)
12b3818b 173
5f402f36 174 def update(self):
d45139f6
MT
175 for protocol in self.protocols:
176 if self.have_address(protocol):
177 self.update_protocol(protocol)
29c8c9c6 178 elif self.can_remove_records:
d45139f6
MT
179 self.remove_protocol(protocol)
180
181 def update_protocol(self, proto):
f22ab085
MT
182 raise NotImplementedError
183
d45139f6 184 def remove_protocol(self, proto):
29c8c9c6
MT
185 if not self.can_remove_records:
186 raise RuntimeError, "can_remove_records is enabled, but remove_protocol() not implemented"
d45139f6 187
29c8c9c6 188 raise NotImplementedError
d45139f6 189
37e24fbf
MT
190 @property
191 def requires_update(self):
192 # If the IP addresses have changed, an update is required
193 if self.ip_address_changed(self.protocols):
194 logger.debug(_("An update for %(hostname)s (%(provider)s)"
195 " is performed because of an IP address change") % \
196 { "hostname" : self.hostname, "provider" : self.name })
197
198 return True
199
200 # If the holdoff time has expired, an update is required, too
201 if self.holdoff_time_expired():
202 logger.debug(_("An update for %(hostname)s (%(provider)s)"
203 " is performed because the holdoff time has expired") % \
204 { "hostname" : self.hostname, "provider" : self.name })
205
206 return True
207
208 # Otherwise, we don't need to perform an update
209 logger.debug(_("No update required for %(hostname)s (%(provider)s)") % \
210 { "hostname" : self.hostname, "provider" : self.name })
211
212 return False
213
112d3fb8
MT
214 @property
215 def has_failure(self):
216 """
217 Returns True when the last update has failed and no retry
218 should be performed, yet.
219 """
220 last_status = self.db.last_update_status(self.hostname)
221
222 # Return False if the last update has not failed.
223 if not last_status == "failure":
224 return False
225
226 # Determine when the holdoff time ends
227 last_update = self.db.last_update(self.hostname, status=last_status)
228 holdoff_end = last_update + datetime.timedelta(days=self.holdoff_failure_days)
229
230 now = datetime.datetime.utcnow()
231 if now < holdoff_end:
232 failure_message = self.db.last_update_failure_message(self.hostname)
233
234 logger.warning(_("An update has not been performed because earlier updates failed for %s") \
235 % self.hostname)
236
237 if failure_message:
238 logger.warning(_("Last failure message:"))
239
240 for line in failure_message.splitlines():
241 logger.warning(" %s" % line)
242
243 logger.warning(_("Further updates will be withheld until %s") % holdoff_end)
244
245 return True
246
247 return False
248
37e24fbf 249 def ip_address_changed(self, protos):
7399fc5b
MT
250 """
251 Returns True if this host is already up to date
252 and does not need to change the IP address on the
253 name server.
254 """
255 for proto in protos:
256 addresses = self.core.system.resolve(self.hostname, proto)
7399fc5b
MT
257 current_address = self.get_address(proto)
258
29c8c9c6
MT
259 # Handle if the system has not got any IP address from a protocol
260 # (i.e. had full dual-stack connectivity which it has not any more)
261 if current_address is None:
262 # If addresses still exists in the DNS system and if this provider
263 # is able to remove records, we will do that.
264 if addresses and self.can_remove_records:
265 return True
266
267 # Otherwise, we cannot go on...
38d81db4
MT
268 continue
269
7399fc5b 270 if not current_address in addresses:
37e24fbf
MT
271 return True
272
273 return False
7399fc5b 274
37e24fbf
MT
275 def holdoff_time_expired(self):
276 """
277 Returns true if the holdoff time has expired
278 and the host requires an update
279 """
280 # If no holdoff days is defined, we cannot go on
281 if not self.holdoff_days:
282 return False
283
284 # Get the timestamp of the last successfull update
112d3fb8 285 last_update = self.db.last_update(self.hostname, status="success")
37e24fbf
MT
286
287 # If no timestamp has been recorded, no update has been
288 # performed. An update should be performed now.
289 if not last_update:
290 return True
7399fc5b 291
37e24fbf
MT
292 # Determine when the holdoff time ends
293 holdoff_end = last_update + datetime.timedelta(days=self.holdoff_days)
294
295 now = datetime.datetime.utcnow()
296
297 if now >= holdoff_end:
298 logger.debug("The holdoff time has expired for %s" % self.hostname)
299 return True
300 else:
301 logger.debug("Updates for %s are held off until %s" % \
302 (self.hostname, holdoff_end))
303 return False
7399fc5b 304
f22ab085
MT
305 def send_request(self, *args, **kwargs):
306 """
307 Proxy connection to the send request
308 method.
309 """
310 return self.core.system.send_request(*args, **kwargs)
311
e3c70807 312 def get_address(self, proto, default=None):
f22ab085
MT
313 """
314 Proxy method to get the current IP address.
315 """
e3c70807 316 return self.core.system.get_address(proto) or default
f22ab085 317
d45139f6
MT
318 def have_address(self, proto):
319 """
320 Returns True if an IP address for the given protocol
321 is known and usable.
322 """
323 address = self.get_address(proto)
324
325 if address:
326 return True
327
328 return False
329
f22ab085 330
5d4bec40
MT
331class DDNSProtocolDynDNS2(object):
332 """
333 This is an abstract class that implements the DynDNS updater
334 protocol version 2. As this is a popular way to update dynamic
335 DNS records, this class is supposed make the provider classes
336 shorter and simpler.
337 """
338
339 # Information about the format of the request is to be found
486c1b9d 340 # http://dyn.com/support/developers/api/perform-update/
5d4bec40
MT
341 # http://dyn.com/support/developers/api/return-codes/
342
29c8c9c6
MT
343 # The DynDNS protocol version 2 does not allow to remove records
344 can_remove_records = False
345
d45139f6 346 def prepare_request_data(self, proto):
5d4bec40
MT
347 data = {
348 "hostname" : self.hostname,
d45139f6 349 "myip" : self.get_address(proto),
5d4bec40
MT
350 }
351
352 return data
353
d45139f6
MT
354 def update_protocol(self, proto):
355 data = self.prepare_request_data(proto)
356
357 return self.send_request(data)
5d4bec40 358
d45139f6 359 def send_request(self, data):
5d4bec40 360 # Send update to the server.
d45139f6 361 response = DDNSProvider.send_request(self, self.url, data=data,
5d4bec40
MT
362 username=self.username, password=self.password)
363
364 # Get the full response message.
365 output = response.read()
366
367 # Handle success messages.
368 if output.startswith("good") or output.startswith("nochg"):
369 return
370
371 # Handle error codes.
372 if output == "badauth":
373 raise DDNSAuthenticationError
af97e369 374 elif output == "abuse":
5d4bec40
MT
375 raise DDNSAbuseError
376 elif output == "notfqdn":
377 raise DDNSRequestError(_("No valid FQDN was given."))
378 elif output == "nohost":
379 raise DDNSRequestError(_("Specified host does not exist."))
380 elif output == "911":
381 raise DDNSInternalServerError
382 elif output == "dnserr":
383 raise DDNSInternalServerError(_("DNS error encountered."))
6ddfd5c7
SS
384 elif output == "badagent":
385 raise DDNSBlockedError
5d4bec40
MT
386
387 # If we got here, some other update error happened.
388 raise DDNSUpdateError(_("Server response: %s") % output)
389
390
78c9780b
SS
391class DDNSResponseParserXML(object):
392 """
393 This class provides a parser for XML responses which
394 will be sent by various providers. This class uses the python
395 shipped XML minidom module to walk through the XML tree and return
396 a requested element.
397 """
398
399 def get_xml_tag_value(self, document, content):
400 # Send input to the parser.
401 xmldoc = xml.dom.minidom.parseString(document)
402
403 # Get XML elements by the given content.
404 element = xmldoc.getElementsByTagName(content)
405
406 # If no element has been found, we directly can return None.
407 if not element:
408 return None
409
410 # Only get the first child from an element, even there are more than one.
411 firstchild = element[0].firstChild
412
413 # Get the value of the child.
414 value = firstchild.nodeValue
415
416 # Return the value.
417 return value
418
419
3b16fdb1 420class DDNSProviderAllInkl(DDNSProvider):
6a11646e
MT
421 handle = "all-inkl.com"
422 name = "All-inkl.com"
423 website = "http://all-inkl.com/"
424 protocols = ("ipv4",)
3b16fdb1
SS
425
426 # There are only information provided by the vendor how to
427 # perform an update on a FRITZ Box. Grab requried informations
428 # from the net.
429 # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
430
431 url = "http://dyndns.kasserver.com"
29c8c9c6 432 can_remove_records = False
3b16fdb1
SS
433
434 def update(self):
3b16fdb1
SS
435 # There is no additional data required so we directly can
436 # send our request.
536e87d1 437 response = self.send_request(self.url, username=self.username, password=self.password)
3b16fdb1
SS
438
439 # Get the full response message.
440 output = response.read()
441
442 # Handle success messages.
443 if output.startswith("good") or output.startswith("nochg"):
444 return
445
446 # If we got here, some other update error happened.
447 raise DDNSUpdateError
448
449
a892c594
MT
450class DDNSProviderBindNsupdate(DDNSProvider):
451 handle = "nsupdate"
452 name = "BIND nsupdate utility"
453 website = "http://en.wikipedia.org/wiki/Nsupdate"
454
455 DEFAULT_TTL = 60
456
64d3fad4
MT
457 @staticmethod
458 def supported():
459 # Search if the nsupdate utility is available
460 paths = os.environ.get("PATH")
461
462 for path in paths.split(":"):
463 executable = os.path.join(path, "nsupdate")
464
465 if os.path.exists(executable):
466 return True
467
468 return False
469
a892c594
MT
470 def update(self):
471 scriptlet = self.__make_scriptlet()
472
473 # -v enables TCP hence we transfer keys and other data that may
474 # exceed the size of one packet.
475 # -t sets the timeout
476 command = ["nsupdate", "-v", "-t", "60"]
477
478 p = subprocess.Popen(command, shell=True,
479 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
480 )
481 stdout, stderr = p.communicate(scriptlet)
482
483 if p.returncode == 0:
484 return
485
486 raise DDNSError("nsupdate terminated with error code: %s\n %s" % (p.returncode, stderr))
487
488 def __make_scriptlet(self):
489 scriptlet = []
490
491 # Set a different server the update is sent to.
492 server = self.get("server", None)
493 if server:
494 scriptlet.append("server %s" % server)
495
97998aac
MW
496 # Set the DNS zone the host should be added to.
497 zone = self.get("zone", None)
498 if zone:
499 scriptlet.append("zone %s" % zone)
500
a892c594
MT
501 key = self.get("key", None)
502 if key:
503 secret = self.get("secret")
504
505 scriptlet.append("key %s %s" % (key, secret))
506
507 ttl = self.get("ttl", self.DEFAULT_TTL)
508
509 # Perform an update for each supported protocol.
510 for rrtype, proto in (("AAAA", "ipv6"), ("A", "ipv4")):
511 address = self.get_address(proto)
512 if not address:
513 continue
514
515 scriptlet.append("update delete %s. %s" % (self.hostname, rrtype))
516 scriptlet.append("update add %s. %s %s %s" % \
517 (self.hostname, ttl, rrtype, address))
518
519 # Send the actions to the server.
520 scriptlet.append("send")
521 scriptlet.append("quit")
522
523 logger.debug(_("Scriptlet:"))
524 for line in scriptlet:
525 # Masquerade the line with the secret key.
526 if line.startswith("key"):
527 line = "key **** ****"
528
529 logger.debug(" %s" % line)
530
531 return "\n".join(scriptlet)
532
533
f3cf1f70 534class DDNSProviderDHS(DDNSProvider):
6a11646e
MT
535 handle = "dhs.org"
536 name = "DHS International"
537 website = "http://dhs.org/"
538 protocols = ("ipv4",)
f3cf1f70
SS
539
540 # No information about the used update api provided on webpage,
541 # grabed from source code of ez-ipudate.
b2b05ef3 542
f3cf1f70 543 url = "http://members.dhs.org/nic/hosts"
29c8c9c6 544 can_remove_records = False
f3cf1f70 545
d45139f6 546 def update_protocol(self, proto):
f3cf1f70
SS
547 data = {
548 "domain" : self.hostname,
d45139f6 549 "ip" : self.get_address(proto),
f3cf1f70
SS
550 "hostcmd" : "edit",
551 "hostcmdstage" : "2",
552 "type" : "4",
553 }
554
555 # Send update to the server.
175c9b80 556 response = self.send_request(self.url, username=self.username, password=self.password,
f3cf1f70
SS
557 data=data)
558
559 # Handle success messages.
560 if response.code == 200:
561 return
562
f3cf1f70
SS
563 # If we got here, some other update error happened.
564 raise DDNSUpdateError
565
566
39301272 567class DDNSProviderDNSpark(DDNSProvider):
6a11646e
MT
568 handle = "dnspark.com"
569 name = "DNS Park"
570 website = "http://dnspark.com/"
571 protocols = ("ipv4",)
39301272
SS
572
573 # Informations to the used api can be found here:
574 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
b2b05ef3 575
39301272 576 url = "https://control.dnspark.com/api/dynamic/update.php"
29c8c9c6 577 can_remove_records = False
39301272 578
d45139f6 579 def update_protocol(self, proto):
39301272
SS
580 data = {
581 "domain" : self.hostname,
d45139f6 582 "ip" : self.get_address(proto),
39301272
SS
583 }
584
585 # Send update to the server.
175c9b80 586 response = self.send_request(self.url, username=self.username, password=self.password,
39301272
SS
587 data=data)
588
589 # Get the full response message.
590 output = response.read()
591
592 # Handle success messages.
593 if output.startswith("ok") or output.startswith("nochange"):
594 return
595
596 # Handle error codes.
597 if output == "unauth":
598 raise DDNSAuthenticationError
599 elif output == "abuse":
600 raise DDNSAbuseError
601 elif output == "blocked":
602 raise DDNSBlockedError
603 elif output == "nofqdn":
604 raise DDNSRequestError(_("No valid FQDN was given."))
605 elif output == "nohost":
606 raise DDNSRequestError(_("Invalid hostname specified."))
607 elif output == "notdyn":
608 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
609 elif output == "invalid":
610 raise DDNSRequestError(_("Invalid IP address has been sent."))
611
612 # If we got here, some other update error happened.
613 raise DDNSUpdateError
614
43b2cd59
SS
615
616class DDNSProviderDtDNS(DDNSProvider):
6a11646e
MT
617 handle = "dtdns.com"
618 name = "DtDNS"
619 website = "http://dtdns.com/"
620 protocols = ("ipv4",)
43b2cd59
SS
621
622 # Information about the format of the HTTPS request is to be found
623 # http://www.dtdns.com/dtsite/updatespec
b2b05ef3 624
43b2cd59 625 url = "https://www.dtdns.com/api/autodns.cfm"
29c8c9c6 626 can_remove_records = False
43b2cd59 627
d45139f6 628 def update_protocol(self, proto):
43b2cd59 629 data = {
d45139f6 630 "ip" : self.get_address(proto),
43b2cd59
SS
631 "id" : self.hostname,
632 "pw" : self.password
633 }
634
635 # Send update to the server.
636 response = self.send_request(self.url, data=data)
637
638 # Get the full response message.
639 output = response.read()
640
641 # Remove all leading and trailing whitespace.
642 output = output.strip()
643
644 # Handle success messages.
645 if "now points to" in output:
646 return
647
648 # Handle error codes.
649 if output == "No hostname to update was supplied.":
650 raise DDNSRequestError(_("No hostname specified."))
651
652 elif output == "The hostname you supplied is not valid.":
653 raise DDNSRequestError(_("Invalid hostname specified."))
654
655 elif output == "The password you supplied is not valid.":
656 raise DDNSAuthenticationError
657
658 elif output == "Administration has disabled this account.":
659 raise DDNSRequestError(_("Account has been disabled."))
660
661 elif output == "Illegal character in IP.":
662 raise DDNSRequestError(_("Invalid IP address has been sent."))
663
664 elif output == "Too many failed requests.":
665 raise DDNSRequestError(_("Too many failed requests."))
666
667 # If we got here, some other update error happened.
668 raise DDNSUpdateError
669
670
5d4bec40 671class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
672 handle = "dyndns.org"
673 name = "Dyn"
674 website = "http://dyn.com/dns/"
675 protocols = ("ipv4",)
bfed6701
SS
676
677 # Information about the format of the request is to be found
678 # http://http://dyn.com/support/developers/api/perform-update/
679 # http://dyn.com/support/developers/api/return-codes/
b2b05ef3 680
bfed6701
SS
681 url = "https://members.dyndns.org/nic/update"
682
bfed6701 683
5d4bec40 684class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
685 handle = "dynu.com"
686 name = "Dynu"
687 website = "http://dynu.com/"
688 protocols = ("ipv6", "ipv4",)
3a8407fa
SS
689
690 # Detailed information about the request and response codes
691 # are available on the providers webpage.
692 # http://dynu.com/Default.aspx?page=dnsapi
693
694 url = "https://api.dynu.com/nic/update"
695
d45139f6
MT
696 # DynU sends the IPv6 and IPv4 address in one request
697
698 def update(self):
699 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
54d3efc8
MT
700
701 # This one supports IPv6
cdc078dc
SS
702 myipv6 = self.get_address("ipv6")
703
704 # Add update information if we have an IPv6 address.
705 if myipv6:
706 data["myipv6"] = myipv6
54d3efc8 707
304026a3 708 self.send_request(data)
3a8407fa
SS
709
710
5d4bec40
MT
711class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
712 handle = "easydns.com"
713 name = "EasyDNS"
714 website = "http://www.easydns.com/"
715 protocols = ("ipv4",)
ee071271
SS
716
717 # There is only some basic documentation provided by the vendor,
718 # also searching the web gain very poor results.
719 # http://mediawiki.easydns.com/index.php/Dynamic_DNS
720
721 url = "http://api.cp.easydns.com/dyn/tomato.php"
722
723
90fe8843
CE
724class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
725 handle = "domopoli.de"
726 name = "domopoli.de"
727 website = "http://domopoli.de/"
728 protocols = ("ipv4",)
729
730 # https://www.domopoli.de/?page=howto#DynDns_start
731
732 url = "http://dyndns.domopoli.de/nic/update"
733
734
a197d1a6
SS
735class DDNSProviderDynsNet(DDNSProvider):
736 handle = "dyns.net"
737 name = "DyNS"
738 website = "http://www.dyns.net/"
739 protocols = ("ipv4",)
29c8c9c6 740 can_remove_records = False
a197d1a6
SS
741
742 # There is very detailed informatio about how to send the update request and
743 # the possible response codes. (Currently we are using the v1.1 proto)
744 # http://www.dyns.net/documentation/technical/protocol/
745
746 url = "http://www.dyns.net/postscript011.php"
747
d45139f6 748 def update_protocol(self, proto):
a197d1a6 749 data = {
d45139f6 750 "ip" : self.get_address(proto),
a197d1a6
SS
751 "host" : self.hostname,
752 "username" : self.username,
753 "password" : self.password,
754 }
755
756 # Send update to the server.
757 response = self.send_request(self.url, data=data)
758
759 # Get the full response message.
760 output = response.read()
761
762 # Handle success messages.
763 if output.startswith("200"):
764 return
765
766 # Handle error codes.
767 if output.startswith("400"):
768 raise DDNSRequestError(_("Malformed request has been sent."))
769 elif output.startswith("401"):
770 raise DDNSAuthenticationError
771 elif output.startswith("402"):
772 raise DDNSRequestError(_("Too frequent update requests have been sent."))
773 elif output.startswith("403"):
774 raise DDNSInternalServerError
775
776 # If we got here, some other update error happened.
777 raise DDNSUpdateError(_("Server response: %s") % output)
778
779
35216523
SS
780class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
781 handle = "enom.com"
782 name = "eNom Inc."
783 website = "http://www.enom.com/"
d45139f6 784 protocols = ("ipv4",)
35216523
SS
785
786 # There are very detailed information about how to send an update request and
787 # the respone codes.
788 # http://www.enom.com/APICommandCatalog/
789
790 url = "https://dynamic.name-services.com/interface.asp"
29c8c9c6 791 can_remove_records = False
35216523 792
d45139f6 793 def update_protocol(self, proto):
35216523
SS
794 data = {
795 "command" : "setdnshost",
796 "responsetype" : "xml",
d45139f6 797 "address" : self.get_address(proto),
35216523
SS
798 "domainpassword" : self.password,
799 "zone" : self.hostname
800 }
801
802 # Send update to the server.
803 response = self.send_request(self.url, data=data)
804
805 # Get the full response message.
806 output = response.read()
807
808 # Handle success messages.
809 if self.get_xml_tag_value(output, "ErrCount") == "0":
810 return
811
812 # Handle error codes.
813 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
814
815 if errorcode == "304155":
816 raise DDNSAuthenticationError
817 elif errorcode == "304153":
818 raise DDNSRequestError(_("Domain not found."))
819
820 # If we got here, some other update error happened.
821 raise DDNSUpdateError
822
823
ab4e352e
SS
824class DDNSProviderEntryDNS(DDNSProvider):
825 handle = "entrydns.net"
826 name = "EntryDNS"
827 website = "http://entrydns.net/"
828 protocols = ("ipv4",)
829
830 # Some very tiny details about their so called "Simple API" can be found
831 # here: https://entrydns.net/help
832 url = "https://entrydns.net/records/modify"
29c8c9c6 833 can_remove_records = False
ab4e352e 834
d45139f6 835 def update_protocol(self, proto):
ab4e352e 836 data = {
d45139f6 837 "ip" : self.get_address(proto),
ab4e352e
SS
838 }
839
840 # Add auth token to the update url.
841 url = "%s/%s" % (self.url, self.token)
842
843 # Send update to the server.
844 try:
babc5e6d 845 response = self.send_request(url, data=data)
ab4e352e
SS
846
847 # Handle error codes
848 except urllib2.HTTPError, e:
849 if e.code == 404:
850 raise DDNSAuthenticationError
851
852 elif e.code == 422:
853 raise DDNSRequestError(_("An invalid IP address was submitted"))
854
855 raise
856
857 # Handle success messages.
858 if response.code == 200:
859 return
860
861 # If we got here, some other update error happened.
862 raise DDNSUpdateError
863
864
aa21a4c6 865class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
6a11646e
MT
866 handle = "freedns.afraid.org"
867 name = "freedns.afraid.org"
868 website = "http://freedns.afraid.org/"
aa21a4c6
SS
869
870 # No information about the request or response could be found on the vendor
871 # page. All used values have been collected by testing.
872 url = "https://freedns.afraid.org/dynamic/update.php"
29c8c9c6 873 can_remove_records = False
aa21a4c6 874
d45139f6 875 def update_protocol(self, proto):
aa21a4c6 876 data = {
d45139f6 877 "address" : self.get_address(proto),
aa21a4c6
SS
878 }
879
880 # Add auth token to the update url.
881 url = "%s?%s" % (self.url, self.token)
882
883 # Send update to the server.
884 response = self.send_request(url, data=data)
885
a204b107
SS
886 # Get the full response message.
887 output = response.read()
888
889 # Handle success messages.
aa21a4c6
SS
890 if output.startswith("Updated") or "has not changed" in output:
891 return
892
893 # Handle error codes.
894 if output == "ERROR: Unable to locate this record":
895 raise DDNSAuthenticationError
896 elif "is an invalid IP address" in output:
897 raise DDNSRequestError(_("Invalid IP address has been sent."))
898
3b524cf2
SS
899 # If we got here, some other update error happened.
900 raise DDNSUpdateError
901
aa21a4c6 902
a08c1b72 903class DDNSProviderLightningWireLabs(DDNSProvider):
6a11646e 904 handle = "dns.lightningwirelabs.com"
fb115fdc 905 name = "Lightning Wire Labs DNS Service"
6a11646e 906 website = "http://dns.lightningwirelabs.com/"
a08c1b72
SS
907
908 # Information about the format of the HTTPS request is to be found
909 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
b2b05ef3 910
a08c1b72
SS
911 url = "https://dns.lightningwirelabs.com/update"
912
5f402f36 913 def update(self):
a08c1b72
SS
914 data = {
915 "hostname" : self.hostname,
e3c70807
MT
916 "address6" : self.get_address("ipv6", "-"),
917 "address4" : self.get_address("ipv4", "-"),
a08c1b72
SS
918 }
919
a08c1b72
SS
920 # Check if a token has been set.
921 if self.token:
922 data["token"] = self.token
923
924 # Check for username and password.
925 elif self.username and self.password:
926 data.update({
927 "username" : self.username,
928 "password" : self.password,
929 })
930
931 # Raise an error if no auth details are given.
932 else:
933 raise DDNSConfigurationError
934
935 # Send update to the server.
cb455540 936 response = self.send_request(self.url, data=data)
a08c1b72
SS
937
938 # Handle success messages.
939 if response.code == 200:
940 return
941
a08c1b72
SS
942 # If we got here, some other update error happened.
943 raise DDNSUpdateError
944
945
446e42af
GH
946class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
947 handle = "myonlineportal.net"
948 name = "myonlineportal.net"
949 website = "https:/myonlineportal.net/"
950
951 # Information about the request and response can be obtained here:
952 # https://myonlineportal.net/howto_dyndns
953
954 url = "https://myonlineportal.net/updateddns"
955
956 def prepare_request_data(self, proto):
957 data = {
958 "hostname" : self.hostname,
959 "ip" : self.get_address(proto),
960 }
961
962 return data
963
964
78c9780b 965class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
6a11646e
MT
966 handle = "namecheap.com"
967 name = "Namecheap"
968 website = "http://namecheap.com"
969 protocols = ("ipv4",)
d1cd57eb
SS
970
971 # Information about the format of the HTTP request is to be found
972 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
973 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
974
975 url = "https://dynamicdns.park-your-domain.com/update"
29c8c9c6 976 can_remove_records = False
d1cd57eb 977
d45139f6 978 def update_protocol(self, proto):
d1cd57eb
SS
979 # Namecheap requires the hostname splitted into a host and domain part.
980 host, domain = self.hostname.split(".", 1)
981
982 data = {
d45139f6 983 "ip" : self.get_address(proto),
d1cd57eb
SS
984 "password" : self.password,
985 "host" : host,
986 "domain" : domain
987 }
988
989 # Send update to the server.
990 response = self.send_request(self.url, data=data)
991
992 # Get the full response message.
993 output = response.read()
994
995 # Handle success messages.
d45139f6 996 if self.get_xml_tag_value(output, "IP") == address:
d1cd57eb
SS
997 return
998
999 # Handle error codes.
78c9780b 1000 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
d1cd57eb
SS
1001
1002 if errorcode == "304156":
1003 raise DDNSAuthenticationError
1004 elif errorcode == "316153":
1005 raise DDNSRequestError(_("Domain not found."))
1006 elif errorcode == "316154":
1007 raise DDNSRequestError(_("Domain not active."))
1008 elif errorcode in ("380098", "380099"):
1009 raise DDNSInternalServerError
1010
1011 # If we got here, some other update error happened.
1012 raise DDNSUpdateError
1013
1014
5d4bec40
MT
1015class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
1016 handle = "no-ip.com"
1017 name = "No-IP"
1018 website = "http://www.no-ip.com/"
1019 protocols = ("ipv4",)
f22ab085
MT
1020
1021 # Information about the format of the HTTP request is to be found
1022 # here: http://www.no-ip.com/integrate/request and
1023 # here: http://www.no-ip.com/integrate/response
1024
88f39629 1025 url = "http://dynupdate.no-ip.com/nic/update"
2de06f59 1026
d45139f6
MT
1027 def prepare_request_data(self, proto):
1028 assert proto == "ipv4"
1029
2de06f59
MT
1030 data = {
1031 "hostname" : self.hostname,
d45139f6 1032 "address" : self.get_address(proto),
f22ab085
MT
1033 }
1034
88f39629 1035 return data
f22ab085
MT
1036
1037
31c95e4b
SS
1038class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
1039 handle = "nsupdate.info"
1040 name = "nsupdate.info"
b9221322 1041 website = "http://nsupdate.info/"
31c95e4b
SS
1042 protocols = ("ipv6", "ipv4",)
1043
1044 # Information about the format of the HTTP request can be found
b9221322 1045 # after login on the provider user interface and here:
31c95e4b
SS
1046 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1047
7b5d382e
SS
1048 url = "https://nsupdate.info/nic/update"
1049
29c8c9c6
MT
1050 # TODO nsupdate.info can actually do this, but the functionality
1051 # has not been implemented here, yet.
1052 can_remove_records = False
1053
31c95e4b
SS
1054 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1055 # and for the password a so called secret.
1056 @property
1057 def username(self):
1058 return self.get("hostname")
1059
1060 @property
1061 def password(self):
9c777232 1062 return self.token or self.get("secret")
31c95e4b 1063
d45139f6 1064 def prepare_request_data(self, proto):
31c95e4b 1065 data = {
d45139f6 1066 "myip" : self.get_address(proto),
31c95e4b
SS
1067 }
1068
1069 return data
1070
1071
90663439
SS
1072class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
1073 handle = "opendns.com"
1074 name = "OpenDNS"
1075 website = "http://www.opendns.com"
1076
1077 # Detailed information about the update request and possible
1078 # response codes can be obtained from here:
1079 # https://support.opendns.com/entries/23891440
1080
1081 url = "https://updates.opendns.com/nic/update"
1082
d45139f6 1083 def prepare_request_data(self, proto):
90663439
SS
1084 data = {
1085 "hostname" : self.hostname,
d45139f6 1086 "myip" : self.get_address(proto),
90663439
SS
1087 }
1088
1089 return data
1090
1091
5d4bec40
MT
1092class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1093 handle = "ovh.com"
1094 name = "OVH"
1095 website = "http://www.ovh.com/"
1096 protocols = ("ipv4",)
a508bda6
SS
1097
1098 # OVH only provides very limited information about how to
1099 # update a DynDNS host. They only provide the update url
1100 # on the their german subpage.
1101 #
1102 # http://hilfe.ovh.de/DomainDynHost
1103
1104 url = "https://www.ovh.com/nic/update"
1105
d45139f6
MT
1106 def prepare_request_data(self, proto):
1107 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
54d3efc8
MT
1108 data.update({
1109 "system" : "dyndns",
1110 })
1111
1112 return data
a508bda6
SS
1113
1114
ef33455e 1115class DDNSProviderRegfish(DDNSProvider):
6a11646e
MT
1116 handle = "regfish.com"
1117 name = "Regfish GmbH"
1118 website = "http://www.regfish.com/"
ef33455e
SS
1119
1120 # A full documentation to the providers api can be found here
1121 # but is only available in german.
1122 # https://www.regfish.de/domains/dyndns/dokumentation
1123
1124 url = "https://dyndns.regfish.de/"
29c8c9c6 1125 can_remove_records = False
ef33455e
SS
1126
1127 def update(self):
1128 data = {
1129 "fqdn" : self.hostname,
1130 }
1131
1132 # Check if we update an IPv6 address.
1133 address6 = self.get_address("ipv6")
1134 if address6:
1135 data["ipv6"] = address6
1136
1137 # Check if we update an IPv4 address.
1138 address4 = self.get_address("ipv4")
1139 if address4:
1140 data["ipv4"] = address4
1141
1142 # Raise an error if none address is given.
1143 if not data.has_key("ipv6") and not data.has_key("ipv4"):
1144 raise DDNSConfigurationError
1145
1146 # Check if a token has been set.
1147 if self.token:
1148 data["token"] = self.token
1149
1150 # Raise an error if no token and no useranem and password
1151 # are given.
1152 elif not self.username and not self.password:
1153 raise DDNSConfigurationError(_("No Auth details specified."))
1154
1155 # HTTP Basic Auth is only allowed if no token is used.
1156 if self.token:
1157 # Send update to the server.
1158 response = self.send_request(self.url, data=data)
1159 else:
1160 # Send update to the server.
1161 response = self.send_request(self.url, username=self.username, password=self.password,
1162 data=data)
1163
1164 # Get the full response message.
1165 output = response.read()
1166
1167 # Handle success messages.
1168 if "100" in output or "101" in output:
1169 return
1170
1171 # Handle error codes.
1172 if "401" or "402" in output:
1173 raise DDNSAuthenticationError
1174 elif "408" in output:
1175 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1176 elif "409" in output:
1177 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1178 elif "412" in output:
1179 raise DDNSRequestError(_("No valid FQDN was given."))
1180 elif "414" in output:
1181 raise DDNSInternalServerError
1182
1183 # If we got here, some other update error happened.
1184 raise DDNSUpdateError
1185
1186
5d4bec40 1187class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
1188 handle = "selfhost.de"
1189 name = "Selfhost.de"
1190 website = "http://www.selfhost.de/"
5d4bec40 1191 protocols = ("ipv4",)
f22ab085 1192
04db1862 1193 url = "https://carol.selfhost.de/nic/update"
f22ab085 1194
d45139f6
MT
1195 def prepare_request_data(self, proto):
1196 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
04db1862
MT
1197 data.update({
1198 "hostname" : "1",
1199 })
f22ab085 1200
04db1862 1201 return data
b09b1545
SS
1202
1203
5d4bec40
MT
1204class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1205 handle = "spdns.org"
1206 name = "SPDNS"
1207 website = "http://spdns.org/"
b09b1545
SS
1208
1209 # Detailed information about request and response codes are provided
1210 # by the vendor. They are using almost the same mechanism and status
1211 # codes as dyndns.org so we can inherit all those stuff.
1212 #
1213 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1214 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1215
1216 url = "https://update.spdns.de/nic/update"
4ec90b93 1217
94ab4379
SS
1218 @property
1219 def username(self):
1220 return self.get("username") or self.hostname
1221
1222 @property
1223 def password(self):
1224 return self.get("username") or self.token
1225
4ec90b93 1226
5d4bec40
MT
1227class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1228 handle = "strato.com"
1229 name = "Strato AG"
1230 website = "http:/www.strato.com/"
1231 protocols = ("ipv4",)
7488825c
SS
1232
1233 # Information about the request and response can be obtained here:
1234 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1235
1236 url = "https://dyndns.strato.com/nic/update"
1237
1238
5d4bec40
MT
1239class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1240 handle = "twodns.de"
1241 name = "TwoDNS"
1242 website = "http://www.twodns.de"
1243 protocols = ("ipv4",)
a6183090
SS
1244
1245 # Detailed information about the request can be found here
1246 # http://twodns.de/en/faqs
1247 # http://twodns.de/en/api
1248
1249 url = "https://update.twodns.de/update"
1250
d45139f6
MT
1251 def prepare_request_data(self, proto):
1252 assert proto == "ipv4"
1253
a6183090 1254 data = {
d45139f6 1255 "ip" : self.get_address(proto),
a6183090
SS
1256 "hostname" : self.hostname
1257 }
1258
1259 return data
1260
1261
5d4bec40
MT
1262class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1263 handle = "udmedia.de"
1264 name = "Udmedia GmbH"
1265 website = "http://www.udmedia.de"
1266 protocols = ("ipv4",)
03bdd188
SS
1267
1268 # Information about the request can be found here
1269 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1270
1271 url = "https://www.udmedia.de/nic/update"
1272
1273
5d4bec40 1274class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
1275 handle = "variomedia.de"
1276 name = "Variomedia"
1277 website = "http://www.variomedia.de/"
1278 protocols = ("ipv6", "ipv4",)
c8c7ca8f
SS
1279
1280 # Detailed information about the request can be found here
1281 # https://dyndns.variomedia.de/
1282
1283 url = "https://dyndns.variomedia.de/nic/update"
1284
d45139f6 1285 def prepare_request_data(self, proto):
c8c7ca8f
SS
1286 data = {
1287 "hostname" : self.hostname,
d45139f6 1288 "myip" : self.get_address(proto),
c8c7ca8f 1289 }
54d3efc8
MT
1290
1291 return data
98fbe467
SS
1292
1293
5d4bec40
MT
1294class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
1295 handle = "zoneedit.com"
1296 name = "Zoneedit"
1297 website = "http://www.zoneedit.com"
1298 protocols = ("ipv4",)
98fbe467
SS
1299
1300 # Detailed information about the request and the response codes can be
1301 # obtained here:
1302 # http://www.zoneedit.com/doc/api/other.html
1303 # http://www.zoneedit.com/faq.html
1304
1305 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1306
d45139f6 1307 def update_protocol(self, proto):
98fbe467 1308 data = {
d45139f6 1309 "dnsto" : self.get_address(proto),
98fbe467
SS
1310 "host" : self.hostname
1311 }
1312
1313 # Send update to the server.
1314 response = self.send_request(self.url, username=self.username, password=self.password,
1315 data=data)
1316
1317 # Get the full response message.
1318 output = response.read()
1319
1320 # Handle success messages.
1321 if output.startswith("<SUCCESS"):
1322 return
1323
1324 # Handle error codes.
1325 if output.startswith("invalid login"):
1326 raise DDNSAuthenticationError
1327 elif output.startswith("<ERROR CODE=\"704\""):
1328 raise DDNSRequestError(_("No valid FQDN was given."))
1329 elif output.startswith("<ERROR CODE=\"702\""):
1330 raise DDNSInternalServerError
1331
1332 # If we got here, some other update error happened.
1333 raise DDNSUpdateError
e53d3225
SS
1334
1335
1336class DDNSProviderZZZZ(DDNSProvider):
1337 handle = "zzzz.io"
1338 name = "zzzz"
1339 website = "https://zzzz.io"
fbdff678 1340 protocols = ("ipv6", "ipv4",)
e53d3225
SS
1341
1342 # Detailed information about the update request can be found here:
1343 # https://zzzz.io/faq/
1344
1345 # Details about the possible response codes have been provided in the bugtracker:
1346 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1347
1348 url = "https://zzzz.io/api/v1/update"
29c8c9c6 1349 can_remove_records = False
e53d3225 1350
d45139f6 1351 def update_protocol(self, proto):
e53d3225 1352 data = {
d45139f6 1353 "ip" : self.get_address(proto),
e53d3225
SS
1354 "token" : self.token,
1355 }
1356
fbdff678
MT
1357 if proto == "ipv6":
1358 data["type"] = "aaaa"
1359
e53d3225
SS
1360 # zzzz uses the host from the full hostname as part
1361 # of the update url.
1362 host, domain = self.hostname.split(".", 1)
1363
1364 # Add host value to the update url.
1365 url = "%s/%s" % (self.url, host)
1366
1367 # Send update to the server.
1368 try:
1369 response = self.send_request(url, data=data)
1370
1371 # Handle error codes.
ff43fa70
MT
1372 except DDNSNotFound:
1373 raise DDNSRequestError(_("Invalid hostname specified"))
e53d3225
SS
1374
1375 # Handle success messages.
1376 if response.code == 200:
1377 return
1378
1379 # If we got here, some other update error happened.
1380 raise DDNSUpdateError