]> git.ipfire.org Git - ddns.git/blame - src/ddns/providers.py
Add changeip.com as new provider.
[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
78046ffe
SS
542class DDNSProviderChangeIP(DDNSProvider):
543 handle = "changeip.com"
544 name = "ChangeIP.com"
545 website = "https://changeip.com"
546 protocols = ("ipv4",)
547
548 # Detailed information about the update api can be found here.
549 # http://www.changeip.com/accounts/knowledgebase.php?action=displayarticle&id=34
550
551 url = "https://nic.changeip.com/nic/update"
552 can_remove_records = False
553
554 def update_protocol(self, proto):
555 data = {
556 "hostname" : self.hostname,
557 "myip" : self.get_address(proto),
558 }
559
560 # Send update to the server.
561 try:
562 response = self.send_request(self.url, username=self.username, password=self.password,
563 data=data)
564
565 # Handle error codes.
566 except urllib2.HTTPError, e:
567 if e.code == 422:
568 raise DDNSRequestError(_("Domain not found."))
569
570 raise
571
572 # Handle success message.
573 if response.code == 200:
574 return
575
576 # If we got here, some other update error happened.
577 raise DDNSUpdateError(_("Server response: %s") % output)
578
579
f3cf1f70 580class DDNSProviderDHS(DDNSProvider):
6a11646e
MT
581 handle = "dhs.org"
582 name = "DHS International"
583 website = "http://dhs.org/"
584 protocols = ("ipv4",)
f3cf1f70
SS
585
586 # No information about the used update api provided on webpage,
587 # grabed from source code of ez-ipudate.
b2b05ef3 588
f3cf1f70 589 url = "http://members.dhs.org/nic/hosts"
29c8c9c6 590 can_remove_records = False
f3cf1f70 591
d45139f6 592 def update_protocol(self, proto):
f3cf1f70
SS
593 data = {
594 "domain" : self.hostname,
d45139f6 595 "ip" : self.get_address(proto),
f3cf1f70
SS
596 "hostcmd" : "edit",
597 "hostcmdstage" : "2",
598 "type" : "4",
599 }
600
601 # Send update to the server.
175c9b80 602 response = self.send_request(self.url, username=self.username, password=self.password,
f3cf1f70
SS
603 data=data)
604
605 # Handle success messages.
606 if response.code == 200:
607 return
608
f3cf1f70
SS
609 # If we got here, some other update error happened.
610 raise DDNSUpdateError
611
612
39301272 613class DDNSProviderDNSpark(DDNSProvider):
6a11646e
MT
614 handle = "dnspark.com"
615 name = "DNS Park"
616 website = "http://dnspark.com/"
617 protocols = ("ipv4",)
39301272
SS
618
619 # Informations to the used api can be found here:
620 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
b2b05ef3 621
39301272 622 url = "https://control.dnspark.com/api/dynamic/update.php"
29c8c9c6 623 can_remove_records = False
39301272 624
d45139f6 625 def update_protocol(self, proto):
39301272
SS
626 data = {
627 "domain" : self.hostname,
d45139f6 628 "ip" : self.get_address(proto),
39301272
SS
629 }
630
631 # Send update to the server.
175c9b80 632 response = self.send_request(self.url, username=self.username, password=self.password,
39301272
SS
633 data=data)
634
635 # Get the full response message.
636 output = response.read()
637
638 # Handle success messages.
639 if output.startswith("ok") or output.startswith("nochange"):
640 return
641
642 # Handle error codes.
643 if output == "unauth":
644 raise DDNSAuthenticationError
645 elif output == "abuse":
646 raise DDNSAbuseError
647 elif output == "blocked":
648 raise DDNSBlockedError
649 elif output == "nofqdn":
650 raise DDNSRequestError(_("No valid FQDN was given."))
651 elif output == "nohost":
652 raise DDNSRequestError(_("Invalid hostname specified."))
653 elif output == "notdyn":
654 raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
655 elif output == "invalid":
656 raise DDNSRequestError(_("Invalid IP address has been sent."))
657
658 # If we got here, some other update error happened.
659 raise DDNSUpdateError
660
43b2cd59
SS
661
662class DDNSProviderDtDNS(DDNSProvider):
6a11646e
MT
663 handle = "dtdns.com"
664 name = "DtDNS"
665 website = "http://dtdns.com/"
666 protocols = ("ipv4",)
43b2cd59
SS
667
668 # Information about the format of the HTTPS request is to be found
669 # http://www.dtdns.com/dtsite/updatespec
b2b05ef3 670
43b2cd59 671 url = "https://www.dtdns.com/api/autodns.cfm"
29c8c9c6 672 can_remove_records = False
43b2cd59 673
d45139f6 674 def update_protocol(self, proto):
43b2cd59 675 data = {
d45139f6 676 "ip" : self.get_address(proto),
43b2cd59
SS
677 "id" : self.hostname,
678 "pw" : self.password
679 }
680
681 # Send update to the server.
682 response = self.send_request(self.url, data=data)
683
684 # Get the full response message.
685 output = response.read()
686
687 # Remove all leading and trailing whitespace.
688 output = output.strip()
689
690 # Handle success messages.
691 if "now points to" in output:
692 return
693
694 # Handle error codes.
695 if output == "No hostname to update was supplied.":
696 raise DDNSRequestError(_("No hostname specified."))
697
698 elif output == "The hostname you supplied is not valid.":
699 raise DDNSRequestError(_("Invalid hostname specified."))
700
701 elif output == "The password you supplied is not valid.":
702 raise DDNSAuthenticationError
703
704 elif output == "Administration has disabled this account.":
705 raise DDNSRequestError(_("Account has been disabled."))
706
707 elif output == "Illegal character in IP.":
708 raise DDNSRequestError(_("Invalid IP address has been sent."))
709
710 elif output == "Too many failed requests.":
711 raise DDNSRequestError(_("Too many failed requests."))
712
713 # If we got here, some other update error happened.
714 raise DDNSUpdateError
715
716
5d4bec40 717class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
718 handle = "dyndns.org"
719 name = "Dyn"
720 website = "http://dyn.com/dns/"
721 protocols = ("ipv4",)
bfed6701
SS
722
723 # Information about the format of the request is to be found
724 # http://http://dyn.com/support/developers/api/perform-update/
725 # http://dyn.com/support/developers/api/return-codes/
b2b05ef3 726
bfed6701
SS
727 url = "https://members.dyndns.org/nic/update"
728
bfed6701 729
5d4bec40 730class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
731 handle = "dynu.com"
732 name = "Dynu"
733 website = "http://dynu.com/"
734 protocols = ("ipv6", "ipv4",)
3a8407fa
SS
735
736 # Detailed information about the request and response codes
737 # are available on the providers webpage.
738 # http://dynu.com/Default.aspx?page=dnsapi
739
740 url = "https://api.dynu.com/nic/update"
741
d45139f6
MT
742 # DynU sends the IPv6 and IPv4 address in one request
743
744 def update(self):
745 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
54d3efc8
MT
746
747 # This one supports IPv6
cdc078dc
SS
748 myipv6 = self.get_address("ipv6")
749
750 # Add update information if we have an IPv6 address.
751 if myipv6:
752 data["myipv6"] = myipv6
54d3efc8 753
304026a3 754 self.send_request(data)
3a8407fa
SS
755
756
5d4bec40
MT
757class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
758 handle = "easydns.com"
759 name = "EasyDNS"
760 website = "http://www.easydns.com/"
761 protocols = ("ipv4",)
ee071271
SS
762
763 # There is only some basic documentation provided by the vendor,
764 # also searching the web gain very poor results.
765 # http://mediawiki.easydns.com/index.php/Dynamic_DNS
766
767 url = "http://api.cp.easydns.com/dyn/tomato.php"
768
769
90fe8843
CE
770class DDNSProviderDomopoli(DDNSProtocolDynDNS2, DDNSProvider):
771 handle = "domopoli.de"
772 name = "domopoli.de"
773 website = "http://domopoli.de/"
774 protocols = ("ipv4",)
775
776 # https://www.domopoli.de/?page=howto#DynDns_start
777
778 url = "http://dyndns.domopoli.de/nic/update"
779
780
a197d1a6
SS
781class DDNSProviderDynsNet(DDNSProvider):
782 handle = "dyns.net"
783 name = "DyNS"
784 website = "http://www.dyns.net/"
785 protocols = ("ipv4",)
29c8c9c6 786 can_remove_records = False
a197d1a6
SS
787
788 # There is very detailed informatio about how to send the update request and
789 # the possible response codes. (Currently we are using the v1.1 proto)
790 # http://www.dyns.net/documentation/technical/protocol/
791
792 url = "http://www.dyns.net/postscript011.php"
793
d45139f6 794 def update_protocol(self, proto):
a197d1a6 795 data = {
d45139f6 796 "ip" : self.get_address(proto),
a197d1a6
SS
797 "host" : self.hostname,
798 "username" : self.username,
799 "password" : self.password,
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 output.startswith("200"):
810 return
811
812 # Handle error codes.
813 if output.startswith("400"):
814 raise DDNSRequestError(_("Malformed request has been sent."))
815 elif output.startswith("401"):
816 raise DDNSAuthenticationError
817 elif output.startswith("402"):
818 raise DDNSRequestError(_("Too frequent update requests have been sent."))
819 elif output.startswith("403"):
820 raise DDNSInternalServerError
821
822 # If we got here, some other update error happened.
823 raise DDNSUpdateError(_("Server response: %s") % output)
824
825
35216523
SS
826class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
827 handle = "enom.com"
828 name = "eNom Inc."
829 website = "http://www.enom.com/"
d45139f6 830 protocols = ("ipv4",)
35216523
SS
831
832 # There are very detailed information about how to send an update request and
833 # the respone codes.
834 # http://www.enom.com/APICommandCatalog/
835
836 url = "https://dynamic.name-services.com/interface.asp"
29c8c9c6 837 can_remove_records = False
35216523 838
d45139f6 839 def update_protocol(self, proto):
35216523
SS
840 data = {
841 "command" : "setdnshost",
842 "responsetype" : "xml",
d45139f6 843 "address" : self.get_address(proto),
35216523
SS
844 "domainpassword" : self.password,
845 "zone" : self.hostname
846 }
847
848 # Send update to the server.
849 response = self.send_request(self.url, data=data)
850
851 # Get the full response message.
852 output = response.read()
853
854 # Handle success messages.
855 if self.get_xml_tag_value(output, "ErrCount") == "0":
856 return
857
858 # Handle error codes.
859 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
860
861 if errorcode == "304155":
862 raise DDNSAuthenticationError
863 elif errorcode == "304153":
864 raise DDNSRequestError(_("Domain not found."))
865
866 # If we got here, some other update error happened.
867 raise DDNSUpdateError
868
869
ab4e352e
SS
870class DDNSProviderEntryDNS(DDNSProvider):
871 handle = "entrydns.net"
872 name = "EntryDNS"
873 website = "http://entrydns.net/"
874 protocols = ("ipv4",)
875
876 # Some very tiny details about their so called "Simple API" can be found
877 # here: https://entrydns.net/help
878 url = "https://entrydns.net/records/modify"
29c8c9c6 879 can_remove_records = False
ab4e352e 880
d45139f6 881 def update_protocol(self, proto):
ab4e352e 882 data = {
d45139f6 883 "ip" : self.get_address(proto),
ab4e352e
SS
884 }
885
886 # Add auth token to the update url.
887 url = "%s/%s" % (self.url, self.token)
888
889 # Send update to the server.
890 try:
babc5e6d 891 response = self.send_request(url, data=data)
ab4e352e
SS
892
893 # Handle error codes
894 except urllib2.HTTPError, e:
895 if e.code == 404:
896 raise DDNSAuthenticationError
897
898 elif e.code == 422:
899 raise DDNSRequestError(_("An invalid IP address was submitted"))
900
901 raise
902
903 # Handle success messages.
904 if response.code == 200:
905 return
906
907 # If we got here, some other update error happened.
908 raise DDNSUpdateError
909
910
aa21a4c6 911class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
6a11646e
MT
912 handle = "freedns.afraid.org"
913 name = "freedns.afraid.org"
914 website = "http://freedns.afraid.org/"
aa21a4c6
SS
915
916 # No information about the request or response could be found on the vendor
917 # page. All used values have been collected by testing.
918 url = "https://freedns.afraid.org/dynamic/update.php"
29c8c9c6 919 can_remove_records = False
aa21a4c6 920
d45139f6 921 def update_protocol(self, proto):
aa21a4c6 922 data = {
d45139f6 923 "address" : self.get_address(proto),
aa21a4c6
SS
924 }
925
926 # Add auth token to the update url.
927 url = "%s?%s" % (self.url, self.token)
928
929 # Send update to the server.
930 response = self.send_request(url, data=data)
931
a204b107
SS
932 # Get the full response message.
933 output = response.read()
934
935 # Handle success messages.
aa21a4c6
SS
936 if output.startswith("Updated") or "has not changed" in output:
937 return
938
939 # Handle error codes.
940 if output == "ERROR: Unable to locate this record":
941 raise DDNSAuthenticationError
942 elif "is an invalid IP address" in output:
943 raise DDNSRequestError(_("Invalid IP address has been sent."))
944
3b524cf2
SS
945 # If we got here, some other update error happened.
946 raise DDNSUpdateError
947
aa21a4c6 948
a08c1b72 949class DDNSProviderLightningWireLabs(DDNSProvider):
6a11646e 950 handle = "dns.lightningwirelabs.com"
fb115fdc 951 name = "Lightning Wire Labs DNS Service"
6a11646e 952 website = "http://dns.lightningwirelabs.com/"
a08c1b72
SS
953
954 # Information about the format of the HTTPS request is to be found
955 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
b2b05ef3 956
a08c1b72
SS
957 url = "https://dns.lightningwirelabs.com/update"
958
5f402f36 959 def update(self):
a08c1b72
SS
960 data = {
961 "hostname" : self.hostname,
e3c70807
MT
962 "address6" : self.get_address("ipv6", "-"),
963 "address4" : self.get_address("ipv4", "-"),
a08c1b72
SS
964 }
965
a08c1b72
SS
966 # Check if a token has been set.
967 if self.token:
968 data["token"] = self.token
969
970 # Check for username and password.
971 elif self.username and self.password:
972 data.update({
973 "username" : self.username,
974 "password" : self.password,
975 })
976
977 # Raise an error if no auth details are given.
978 else:
979 raise DDNSConfigurationError
980
981 # Send update to the server.
cb455540 982 response = self.send_request(self.url, data=data)
a08c1b72
SS
983
984 # Handle success messages.
985 if response.code == 200:
986 return
987
a08c1b72
SS
988 # If we got here, some other update error happened.
989 raise DDNSUpdateError
990
991
446e42af
GH
992class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
993 handle = "myonlineportal.net"
994 name = "myonlineportal.net"
995 website = "https:/myonlineportal.net/"
996
997 # Information about the request and response can be obtained here:
998 # https://myonlineportal.net/howto_dyndns
999
1000 url = "https://myonlineportal.net/updateddns"
1001
1002 def prepare_request_data(self, proto):
1003 data = {
1004 "hostname" : self.hostname,
1005 "ip" : self.get_address(proto),
1006 }
1007
1008 return data
1009
1010
78c9780b 1011class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
6a11646e
MT
1012 handle = "namecheap.com"
1013 name = "Namecheap"
1014 website = "http://namecheap.com"
1015 protocols = ("ipv4",)
d1cd57eb
SS
1016
1017 # Information about the format of the HTTP request is to be found
1018 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
1019 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
1020
1021 url = "https://dynamicdns.park-your-domain.com/update"
29c8c9c6 1022 can_remove_records = False
d1cd57eb 1023
d45139f6 1024 def update_protocol(self, proto):
d1cd57eb
SS
1025 # Namecheap requires the hostname splitted into a host and domain part.
1026 host, domain = self.hostname.split(".", 1)
1027
1028 data = {
d45139f6 1029 "ip" : self.get_address(proto),
d1cd57eb
SS
1030 "password" : self.password,
1031 "host" : host,
1032 "domain" : domain
1033 }
1034
1035 # Send update to the server.
1036 response = self.send_request(self.url, data=data)
1037
1038 # Get the full response message.
1039 output = response.read()
1040
1041 # Handle success messages.
d45139f6 1042 if self.get_xml_tag_value(output, "IP") == address:
d1cd57eb
SS
1043 return
1044
1045 # Handle error codes.
78c9780b 1046 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
d1cd57eb
SS
1047
1048 if errorcode == "304156":
1049 raise DDNSAuthenticationError
1050 elif errorcode == "316153":
1051 raise DDNSRequestError(_("Domain not found."))
1052 elif errorcode == "316154":
1053 raise DDNSRequestError(_("Domain not active."))
1054 elif errorcode in ("380098", "380099"):
1055 raise DDNSInternalServerError
1056
1057 # If we got here, some other update error happened.
1058 raise DDNSUpdateError
1059
1060
5d4bec40
MT
1061class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
1062 handle = "no-ip.com"
1063 name = "No-IP"
1064 website = "http://www.no-ip.com/"
1065 protocols = ("ipv4",)
f22ab085
MT
1066
1067 # Information about the format of the HTTP request is to be found
1068 # here: http://www.no-ip.com/integrate/request and
1069 # here: http://www.no-ip.com/integrate/response
1070
88f39629 1071 url = "http://dynupdate.no-ip.com/nic/update"
2de06f59 1072
d45139f6
MT
1073 def prepare_request_data(self, proto):
1074 assert proto == "ipv4"
1075
2de06f59
MT
1076 data = {
1077 "hostname" : self.hostname,
d45139f6 1078 "address" : self.get_address(proto),
f22ab085
MT
1079 }
1080
88f39629 1081 return data
f22ab085
MT
1082
1083
31c95e4b
SS
1084class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
1085 handle = "nsupdate.info"
1086 name = "nsupdate.info"
b9221322 1087 website = "http://nsupdate.info/"
31c95e4b
SS
1088 protocols = ("ipv6", "ipv4",)
1089
1090 # Information about the format of the HTTP request can be found
b9221322 1091 # after login on the provider user interface and here:
31c95e4b
SS
1092 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
1093
7b5d382e
SS
1094 url = "https://nsupdate.info/nic/update"
1095
29c8c9c6
MT
1096 # TODO nsupdate.info can actually do this, but the functionality
1097 # has not been implemented here, yet.
1098 can_remove_records = False
1099
64018439
MT
1100 # After a failed update, there will be no retries
1101 # https://bugzilla.ipfire.org/show_bug.cgi?id=10603
1102 holdoff_failure_days = None
1103
31c95e4b
SS
1104 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1105 # and for the password a so called secret.
1106 @property
1107 def username(self):
1108 return self.get("hostname")
1109
1110 @property
1111 def password(self):
9c777232 1112 return self.token or self.get("secret")
31c95e4b 1113
d45139f6 1114 def prepare_request_data(self, proto):
31c95e4b 1115 data = {
d45139f6 1116 "myip" : self.get_address(proto),
31c95e4b
SS
1117 }
1118
1119 return data
1120
1121
90663439
SS
1122class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
1123 handle = "opendns.com"
1124 name = "OpenDNS"
1125 website = "http://www.opendns.com"
1126
1127 # Detailed information about the update request and possible
1128 # response codes can be obtained from here:
1129 # https://support.opendns.com/entries/23891440
1130
1131 url = "https://updates.opendns.com/nic/update"
1132
d45139f6 1133 def prepare_request_data(self, proto):
90663439
SS
1134 data = {
1135 "hostname" : self.hostname,
d45139f6 1136 "myip" : self.get_address(proto),
90663439
SS
1137 }
1138
1139 return data
1140
1141
5d4bec40
MT
1142class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1143 handle = "ovh.com"
1144 name = "OVH"
1145 website = "http://www.ovh.com/"
1146 protocols = ("ipv4",)
a508bda6
SS
1147
1148 # OVH only provides very limited information about how to
1149 # update a DynDNS host. They only provide the update url
1150 # on the their german subpage.
1151 #
1152 # http://hilfe.ovh.de/DomainDynHost
1153
1154 url = "https://www.ovh.com/nic/update"
1155
d45139f6
MT
1156 def prepare_request_data(self, proto):
1157 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
54d3efc8
MT
1158 data.update({
1159 "system" : "dyndns",
1160 })
1161
1162 return data
a508bda6
SS
1163
1164
ef33455e 1165class DDNSProviderRegfish(DDNSProvider):
6a11646e
MT
1166 handle = "regfish.com"
1167 name = "Regfish GmbH"
1168 website = "http://www.regfish.com/"
ef33455e
SS
1169
1170 # A full documentation to the providers api can be found here
1171 # but is only available in german.
1172 # https://www.regfish.de/domains/dyndns/dokumentation
1173
1174 url = "https://dyndns.regfish.de/"
29c8c9c6 1175 can_remove_records = False
ef33455e
SS
1176
1177 def update(self):
1178 data = {
1179 "fqdn" : self.hostname,
1180 }
1181
1182 # Check if we update an IPv6 address.
1183 address6 = self.get_address("ipv6")
1184 if address6:
1185 data["ipv6"] = address6
1186
1187 # Check if we update an IPv4 address.
1188 address4 = self.get_address("ipv4")
1189 if address4:
1190 data["ipv4"] = address4
1191
1192 # Raise an error if none address is given.
1193 if not data.has_key("ipv6") and not data.has_key("ipv4"):
1194 raise DDNSConfigurationError
1195
1196 # Check if a token has been set.
1197 if self.token:
1198 data["token"] = self.token
1199
1200 # Raise an error if no token and no useranem and password
1201 # are given.
1202 elif not self.username and not self.password:
1203 raise DDNSConfigurationError(_("No Auth details specified."))
1204
1205 # HTTP Basic Auth is only allowed if no token is used.
1206 if self.token:
1207 # Send update to the server.
1208 response = self.send_request(self.url, data=data)
1209 else:
1210 # Send update to the server.
1211 response = self.send_request(self.url, username=self.username, password=self.password,
1212 data=data)
1213
1214 # Get the full response message.
1215 output = response.read()
1216
1217 # Handle success messages.
1218 if "100" in output or "101" in output:
1219 return
1220
1221 # Handle error codes.
1222 if "401" or "402" in output:
1223 raise DDNSAuthenticationError
1224 elif "408" in output:
1225 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1226 elif "409" in output:
1227 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1228 elif "412" in output:
1229 raise DDNSRequestError(_("No valid FQDN was given."))
1230 elif "414" in output:
1231 raise DDNSInternalServerError
1232
1233 # If we got here, some other update error happened.
1234 raise DDNSUpdateError
1235
1236
5d4bec40 1237class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
1238 handle = "selfhost.de"
1239 name = "Selfhost.de"
1240 website = "http://www.selfhost.de/"
5d4bec40 1241 protocols = ("ipv4",)
f22ab085 1242
04db1862 1243 url = "https://carol.selfhost.de/nic/update"
f22ab085 1244
d45139f6
MT
1245 def prepare_request_data(self, proto):
1246 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
04db1862
MT
1247 data.update({
1248 "hostname" : "1",
1249 })
f22ab085 1250
04db1862 1251 return data
b09b1545
SS
1252
1253
5d4bec40
MT
1254class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1255 handle = "spdns.org"
1256 name = "SPDNS"
1257 website = "http://spdns.org/"
b09b1545
SS
1258
1259 # Detailed information about request and response codes are provided
1260 # by the vendor. They are using almost the same mechanism and status
1261 # codes as dyndns.org so we can inherit all those stuff.
1262 #
1263 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1264 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1265
1266 url = "https://update.spdns.de/nic/update"
4ec90b93 1267
94ab4379
SS
1268 @property
1269 def username(self):
1270 return self.get("username") or self.hostname
1271
1272 @property
1273 def password(self):
1274 return self.get("username") or self.token
1275
4ec90b93 1276
5d4bec40
MT
1277class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1278 handle = "strato.com"
1279 name = "Strato AG"
1280 website = "http:/www.strato.com/"
1281 protocols = ("ipv4",)
7488825c
SS
1282
1283 # Information about the request and response can be obtained here:
1284 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1285
1286 url = "https://dyndns.strato.com/nic/update"
1287
1288
5d4bec40
MT
1289class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1290 handle = "twodns.de"
1291 name = "TwoDNS"
1292 website = "http://www.twodns.de"
1293 protocols = ("ipv4",)
a6183090
SS
1294
1295 # Detailed information about the request can be found here
1296 # http://twodns.de/en/faqs
1297 # http://twodns.de/en/api
1298
1299 url = "https://update.twodns.de/update"
1300
d45139f6
MT
1301 def prepare_request_data(self, proto):
1302 assert proto == "ipv4"
1303
a6183090 1304 data = {
d45139f6 1305 "ip" : self.get_address(proto),
a6183090
SS
1306 "hostname" : self.hostname
1307 }
1308
1309 return data
1310
1311
5d4bec40
MT
1312class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1313 handle = "udmedia.de"
1314 name = "Udmedia GmbH"
1315 website = "http://www.udmedia.de"
1316 protocols = ("ipv4",)
03bdd188
SS
1317
1318 # Information about the request can be found here
1319 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1320
1321 url = "https://www.udmedia.de/nic/update"
1322
1323
5d4bec40 1324class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
1325 handle = "variomedia.de"
1326 name = "Variomedia"
1327 website = "http://www.variomedia.de/"
1328 protocols = ("ipv6", "ipv4",)
c8c7ca8f
SS
1329
1330 # Detailed information about the request can be found here
1331 # https://dyndns.variomedia.de/
1332
1333 url = "https://dyndns.variomedia.de/nic/update"
1334
d45139f6 1335 def prepare_request_data(self, proto):
c8c7ca8f
SS
1336 data = {
1337 "hostname" : self.hostname,
d45139f6 1338 "myip" : self.get_address(proto),
c8c7ca8f 1339 }
54d3efc8
MT
1340
1341 return data
98fbe467
SS
1342
1343
5d4bec40
MT
1344class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
1345 handle = "zoneedit.com"
1346 name = "Zoneedit"
1347 website = "http://www.zoneedit.com"
1348 protocols = ("ipv4",)
98fbe467
SS
1349
1350 # Detailed information about the request and the response codes can be
1351 # obtained here:
1352 # http://www.zoneedit.com/doc/api/other.html
1353 # http://www.zoneedit.com/faq.html
1354
1355 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1356
d45139f6 1357 def update_protocol(self, proto):
98fbe467 1358 data = {
d45139f6 1359 "dnsto" : self.get_address(proto),
98fbe467
SS
1360 "host" : self.hostname
1361 }
1362
1363 # Send update to the server.
1364 response = self.send_request(self.url, username=self.username, password=self.password,
1365 data=data)
1366
1367 # Get the full response message.
1368 output = response.read()
1369
1370 # Handle success messages.
1371 if output.startswith("<SUCCESS"):
1372 return
1373
1374 # Handle error codes.
1375 if output.startswith("invalid login"):
1376 raise DDNSAuthenticationError
1377 elif output.startswith("<ERROR CODE=\"704\""):
1378 raise DDNSRequestError(_("No valid FQDN was given."))
1379 elif output.startswith("<ERROR CODE=\"702\""):
1380 raise DDNSInternalServerError
1381
1382 # If we got here, some other update error happened.
1383 raise DDNSUpdateError
e53d3225
SS
1384
1385
1386class DDNSProviderZZZZ(DDNSProvider):
1387 handle = "zzzz.io"
1388 name = "zzzz"
1389 website = "https://zzzz.io"
fbdff678 1390 protocols = ("ipv6", "ipv4",)
e53d3225
SS
1391
1392 # Detailed information about the update request can be found here:
1393 # https://zzzz.io/faq/
1394
1395 # Details about the possible response codes have been provided in the bugtracker:
1396 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1397
1398 url = "https://zzzz.io/api/v1/update"
29c8c9c6 1399 can_remove_records = False
e53d3225 1400
d45139f6 1401 def update_protocol(self, proto):
e53d3225 1402 data = {
d45139f6 1403 "ip" : self.get_address(proto),
e53d3225
SS
1404 "token" : self.token,
1405 }
1406
fbdff678
MT
1407 if proto == "ipv6":
1408 data["type"] = "aaaa"
1409
e53d3225
SS
1410 # zzzz uses the host from the full hostname as part
1411 # of the update url.
1412 host, domain = self.hostname.split(".", 1)
1413
1414 # Add host value to the update url.
1415 url = "%s/%s" % (self.url, host)
1416
1417 # Send update to the server.
1418 try:
1419 response = self.send_request(url, data=data)
1420
1421 # Handle error codes.
ff43fa70
MT
1422 except DDNSNotFound:
1423 raise DDNSRequestError(_("Invalid hostname specified"))
e53d3225
SS
1424
1425 # Handle success messages.
1426 if response.code == 200:
1427 return
1428
1429 # If we got here, some other update error happened.
1430 raise DDNSUpdateError