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