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