]> git.ipfire.org Git - ddns.git/blame - src/ddns/providers.py
Update translations
[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
7b5d382e
SS
1009 url = "https://nsupdate.info/nic/update"
1010
29c8c9c6
MT
1011 # TODO nsupdate.info can actually do this, but the functionality
1012 # has not been implemented here, yet.
1013 can_remove_records = False
1014
31c95e4b
SS
1015 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
1016 # and for the password a so called secret.
1017 @property
1018 def username(self):
1019 return self.get("hostname")
1020
1021 @property
1022 def password(self):
9c777232 1023 return self.token or self.get("secret")
31c95e4b 1024
d45139f6 1025 def prepare_request_data(self, proto):
31c95e4b 1026 data = {
d45139f6 1027 "myip" : self.get_address(proto),
31c95e4b
SS
1028 }
1029
1030 return data
1031
1032
90663439
SS
1033class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
1034 handle = "opendns.com"
1035 name = "OpenDNS"
1036 website = "http://www.opendns.com"
1037
1038 # Detailed information about the update request and possible
1039 # response codes can be obtained from here:
1040 # https://support.opendns.com/entries/23891440
1041
1042 url = "https://updates.opendns.com/nic/update"
1043
d45139f6 1044 def prepare_request_data(self, proto):
90663439
SS
1045 data = {
1046 "hostname" : self.hostname,
d45139f6 1047 "myip" : self.get_address(proto),
90663439
SS
1048 }
1049
1050 return data
1051
1052
5d4bec40
MT
1053class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1054 handle = "ovh.com"
1055 name = "OVH"
1056 website = "http://www.ovh.com/"
1057 protocols = ("ipv4",)
a508bda6
SS
1058
1059 # OVH only provides very limited information about how to
1060 # update a DynDNS host. They only provide the update url
1061 # on the their german subpage.
1062 #
1063 # http://hilfe.ovh.de/DomainDynHost
1064
1065 url = "https://www.ovh.com/nic/update"
1066
d45139f6
MT
1067 def prepare_request_data(self, proto):
1068 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
54d3efc8
MT
1069 data.update({
1070 "system" : "dyndns",
1071 })
1072
1073 return data
a508bda6
SS
1074
1075
ef33455e 1076class DDNSProviderRegfish(DDNSProvider):
6a11646e
MT
1077 handle = "regfish.com"
1078 name = "Regfish GmbH"
1079 website = "http://www.regfish.com/"
ef33455e
SS
1080
1081 # A full documentation to the providers api can be found here
1082 # but is only available in german.
1083 # https://www.regfish.de/domains/dyndns/dokumentation
1084
1085 url = "https://dyndns.regfish.de/"
29c8c9c6 1086 can_remove_records = False
ef33455e
SS
1087
1088 def update(self):
1089 data = {
1090 "fqdn" : self.hostname,
1091 }
1092
1093 # Check if we update an IPv6 address.
1094 address6 = self.get_address("ipv6")
1095 if address6:
1096 data["ipv6"] = address6
1097
1098 # Check if we update an IPv4 address.
1099 address4 = self.get_address("ipv4")
1100 if address4:
1101 data["ipv4"] = address4
1102
1103 # Raise an error if none address is given.
1104 if not data.has_key("ipv6") and not data.has_key("ipv4"):
1105 raise DDNSConfigurationError
1106
1107 # Check if a token has been set.
1108 if self.token:
1109 data["token"] = self.token
1110
1111 # Raise an error if no token and no useranem and password
1112 # are given.
1113 elif not self.username and not self.password:
1114 raise DDNSConfigurationError(_("No Auth details specified."))
1115
1116 # HTTP Basic Auth is only allowed if no token is used.
1117 if self.token:
1118 # Send update to the server.
1119 response = self.send_request(self.url, data=data)
1120 else:
1121 # Send update to the server.
1122 response = self.send_request(self.url, username=self.username, password=self.password,
1123 data=data)
1124
1125 # Get the full response message.
1126 output = response.read()
1127
1128 # Handle success messages.
1129 if "100" in output or "101" in output:
1130 return
1131
1132 # Handle error codes.
1133 if "401" or "402" in output:
1134 raise DDNSAuthenticationError
1135 elif "408" in output:
1136 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1137 elif "409" in output:
1138 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1139 elif "412" in output:
1140 raise DDNSRequestError(_("No valid FQDN was given."))
1141 elif "414" in output:
1142 raise DDNSInternalServerError
1143
1144 # If we got here, some other update error happened.
1145 raise DDNSUpdateError
1146
1147
5d4bec40 1148class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
1149 handle = "selfhost.de"
1150 name = "Selfhost.de"
1151 website = "http://www.selfhost.de/"
5d4bec40 1152 protocols = ("ipv4",)
f22ab085 1153
04db1862 1154 url = "https://carol.selfhost.de/nic/update"
f22ab085 1155
d45139f6
MT
1156 def prepare_request_data(self, proto):
1157 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
04db1862
MT
1158 data.update({
1159 "hostname" : "1",
1160 })
f22ab085 1161
04db1862 1162 return data
b09b1545
SS
1163
1164
5d4bec40
MT
1165class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1166 handle = "spdns.org"
1167 name = "SPDNS"
1168 website = "http://spdns.org/"
b09b1545
SS
1169
1170 # Detailed information about request and response codes are provided
1171 # by the vendor. They are using almost the same mechanism and status
1172 # codes as dyndns.org so we can inherit all those stuff.
1173 #
1174 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1175 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1176
1177 url = "https://update.spdns.de/nic/update"
4ec90b93 1178
94ab4379
SS
1179 @property
1180 def username(self):
1181 return self.get("username") or self.hostname
1182
1183 @property
1184 def password(self):
1185 return self.get("username") or self.token
1186
4ec90b93 1187
5d4bec40
MT
1188class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1189 handle = "strato.com"
1190 name = "Strato AG"
1191 website = "http:/www.strato.com/"
1192 protocols = ("ipv4",)
7488825c
SS
1193
1194 # Information about the request and response can be obtained here:
1195 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1196
1197 url = "https://dyndns.strato.com/nic/update"
1198
1199
5d4bec40
MT
1200class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1201 handle = "twodns.de"
1202 name = "TwoDNS"
1203 website = "http://www.twodns.de"
1204 protocols = ("ipv4",)
a6183090
SS
1205
1206 # Detailed information about the request can be found here
1207 # http://twodns.de/en/faqs
1208 # http://twodns.de/en/api
1209
1210 url = "https://update.twodns.de/update"
1211
d45139f6
MT
1212 def prepare_request_data(self, proto):
1213 assert proto == "ipv4"
1214
a6183090 1215 data = {
d45139f6 1216 "ip" : self.get_address(proto),
a6183090
SS
1217 "hostname" : self.hostname
1218 }
1219
1220 return data
1221
1222
5d4bec40
MT
1223class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1224 handle = "udmedia.de"
1225 name = "Udmedia GmbH"
1226 website = "http://www.udmedia.de"
1227 protocols = ("ipv4",)
03bdd188
SS
1228
1229 # Information about the request can be found here
1230 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1231
1232 url = "https://www.udmedia.de/nic/update"
1233
1234
5d4bec40 1235class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
6a11646e
MT
1236 handle = "variomedia.de"
1237 name = "Variomedia"
1238 website = "http://www.variomedia.de/"
1239 protocols = ("ipv6", "ipv4",)
c8c7ca8f
SS
1240
1241 # Detailed information about the request can be found here
1242 # https://dyndns.variomedia.de/
1243
1244 url = "https://dyndns.variomedia.de/nic/update"
1245
d45139f6 1246 def prepare_request_data(self, proto):
c8c7ca8f
SS
1247 data = {
1248 "hostname" : self.hostname,
d45139f6 1249 "myip" : self.get_address(proto),
c8c7ca8f 1250 }
54d3efc8
MT
1251
1252 return data
98fbe467
SS
1253
1254
5d4bec40
MT
1255class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
1256 handle = "zoneedit.com"
1257 name = "Zoneedit"
1258 website = "http://www.zoneedit.com"
1259 protocols = ("ipv4",)
98fbe467
SS
1260
1261 # Detailed information about the request and the response codes can be
1262 # obtained here:
1263 # http://www.zoneedit.com/doc/api/other.html
1264 # http://www.zoneedit.com/faq.html
1265
1266 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1267
d45139f6 1268 def update_protocol(self, proto):
98fbe467 1269 data = {
d45139f6 1270 "dnsto" : self.get_address(proto),
98fbe467
SS
1271 "host" : self.hostname
1272 }
1273
1274 # Send update to the server.
1275 response = self.send_request(self.url, username=self.username, password=self.password,
1276 data=data)
1277
1278 # Get the full response message.
1279 output = response.read()
1280
1281 # Handle success messages.
1282 if output.startswith("<SUCCESS"):
1283 return
1284
1285 # Handle error codes.
1286 if output.startswith("invalid login"):
1287 raise DDNSAuthenticationError
1288 elif output.startswith("<ERROR CODE=\"704\""):
1289 raise DDNSRequestError(_("No valid FQDN was given."))
1290 elif output.startswith("<ERROR CODE=\"702\""):
1291 raise DDNSInternalServerError
1292
1293 # If we got here, some other update error happened.
1294 raise DDNSUpdateError
e53d3225
SS
1295
1296
1297class DDNSProviderZZZZ(DDNSProvider):
1298 handle = "zzzz.io"
1299 name = "zzzz"
1300 website = "https://zzzz.io"
fbdff678 1301 protocols = ("ipv6", "ipv4",)
e53d3225
SS
1302
1303 # Detailed information about the update request can be found here:
1304 # https://zzzz.io/faq/
1305
1306 # Details about the possible response codes have been provided in the bugtracker:
1307 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1308
1309 url = "https://zzzz.io/api/v1/update"
29c8c9c6 1310 can_remove_records = False
e53d3225 1311
d45139f6 1312 def update_protocol(self, proto):
e53d3225 1313 data = {
d45139f6 1314 "ip" : self.get_address(proto),
e53d3225
SS
1315 "token" : self.token,
1316 }
1317
fbdff678
MT
1318 if proto == "ipv6":
1319 data["type"] = "aaaa"
1320
e53d3225
SS
1321 # zzzz uses the host from the full hostname as part
1322 # of the update url.
1323 host, domain = self.hostname.split(".", 1)
1324
1325 # Add host value to the update url.
1326 url = "%s/%s" % (self.url, host)
1327
1328 # Send update to the server.
1329 try:
1330 response = self.send_request(url, data=data)
1331
1332 # Handle error codes.
ff43fa70
MT
1333 except DDNSNotFound:
1334 raise DDNSRequestError(_("Invalid hostname specified"))
e53d3225
SS
1335
1336 # Handle success messages.
1337 if response.code == 200:
1338 return
1339
1340 # If we got here, some other update error happened.
1341 raise DDNSUpdateError