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