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