]> git.ipfire.org Git - ddns.git/blob - src/ddns/providers.py
c6ef5c0d7c0cd0a22e4f43d8ead72615e35d6e25
[ddns.git] / src / ddns / providers.py
1 #!/usr/bin/python
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 ###############################################################################
21
22 import datetime
23 import logging
24 import subprocess
25 import urllib2
26 import xml.dom.minidom
27
28 from i18n import _
29
30 # Import all possible exception types.
31 from .errors import *
32
33 logger = logging.getLogger("ddns.providers")
34 logger.propagate = 1
35
36 _providers = {}
37
38 def get():
39 """
40 Returns a dict with all automatically registered providers.
41 """
42 return _providers.copy()
43
44 class DDNSProvider(object):
45 # A short string that uniquely identifies
46 # this provider.
47 handle = None
48
49 # The full name of the provider.
50 name = None
51
52 # A weburl to the homepage of the provider.
53 # (Where to register a new account?)
54 website = None
55
56 # A list of supported protocols.
57 protocols = ("ipv6", "ipv4")
58
59 DEFAULT_SETTINGS = {}
60
61 # holdoff time - Number of days no update is performed unless
62 # the IP address has changed.
63 holdoff_days = 30
64
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
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
97 @property
98 def db(self):
99 return self.core.db
100
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
128 @property
129 def token(self):
130 """
131 Fast access to the token.
132 """
133 return self.get("token")
134
135 def __call__(self, force=False):
136 if force:
137 logger.debug(_("Updating %s forced") % self.hostname)
138
139 # Do nothing if no update is required
140 elif not self.requires_update:
141 return
142
143 # Execute the update.
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
152
153 logger.info(_("Dynamic DNS update for %(hostname)s (%(provider)s) successful") % \
154 { "hostname" : self.hostname, "provider" : self.name })
155 self.core.db.log_success(self.hostname)
156
157 def update(self):
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):
165 raise NotImplementedError
166
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
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):
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
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
215 if not current_address in addresses:
216 return True
217
218 return False
219
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
249
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
257 def get_address(self, proto, default=None):
258 """
259 Proxy method to get the current IP address.
260 """
261 return self.core.system.get_address(proto) or default
262
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
275
276 class 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
285 # http://dyn.com/support/developers/api/perform-update/
286 # http://dyn.com/support/developers/api/return-codes/
287
288 def prepare_request_data(self, proto):
289 data = {
290 "hostname" : self.hostname,
291 "myip" : self.get_address(proto),
292 }
293
294 return data
295
296 def update_protocol(self, proto):
297 data = self.prepare_request_data(proto)
298
299 return self.send_request(data)
300
301 def send_request(self, data):
302 # Send update to the server.
303 response = DDNSProvider.send_request(self, self.url, data=data,
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
316 elif output == "abuse":
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."))
326 elif output == "badagent":
327 raise DDNSBlockedError
328
329 # If we got here, some other update error happened.
330 raise DDNSUpdateError(_("Server response: %s") % output)
331
332
333 class 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
362 class DDNSProviderAllInkl(DDNSProvider):
363 handle = "all-inkl.com"
364 name = "All-inkl.com"
365 website = "http://all-inkl.com/"
366 protocols = ("ipv4",)
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):
376 # There is no additional data required so we directly can
377 # send our request.
378 response = self.send_request(self.url, username=self.username, password=self.password)
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
391 class 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
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
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
462 class DDNSProviderDHS(DDNSProvider):
463 handle = "dhs.org"
464 name = "DHS International"
465 website = "http://dhs.org/"
466 protocols = ("ipv4",)
467
468 # No information about the used update api provided on webpage,
469 # grabed from source code of ez-ipudate.
470
471 url = "http://members.dhs.org/nic/hosts"
472
473 def update_protocol(self, proto):
474 data = {
475 "domain" : self.hostname,
476 "ip" : self.get_address(proto),
477 "hostcmd" : "edit",
478 "hostcmdstage" : "2",
479 "type" : "4",
480 }
481
482 # Send update to the server.
483 response = self.send_request(self.url, username=self.username, password=self.password,
484 data=data)
485
486 # Handle success messages.
487 if response.code == 200:
488 return
489
490 # If we got here, some other update error happened.
491 raise DDNSUpdateError
492
493
494 class DDNSProviderDNSpark(DDNSProvider):
495 handle = "dnspark.com"
496 name = "DNS Park"
497 website = "http://dnspark.com/"
498 protocols = ("ipv4",)
499
500 # Informations to the used api can be found here:
501 # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
502
503 url = "https://control.dnspark.com/api/dynamic/update.php"
504
505 def update_protocol(self, proto):
506 data = {
507 "domain" : self.hostname,
508 "ip" : self.get_address(proto),
509 }
510
511 # Send update to the server.
512 response = self.send_request(self.url, username=self.username, password=self.password,
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
541
542 class DDNSProviderDtDNS(DDNSProvider):
543 handle = "dtdns.com"
544 name = "DtDNS"
545 website = "http://dtdns.com/"
546 protocols = ("ipv4",)
547
548 # Information about the format of the HTTPS request is to be found
549 # http://www.dtdns.com/dtsite/updatespec
550
551 url = "https://www.dtdns.com/api/autodns.cfm"
552
553 def update_protocol(self, proto):
554 data = {
555 "ip" : self.get_address(proto),
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
596 class DDNSProviderDynDNS(DDNSProtocolDynDNS2, DDNSProvider):
597 handle = "dyndns.org"
598 name = "Dyn"
599 website = "http://dyn.com/dns/"
600 protocols = ("ipv4",)
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/
605
606 url = "https://members.dyndns.org/nic/update"
607
608
609 class DDNSProviderDynU(DDNSProtocolDynDNS2, DDNSProvider):
610 handle = "dynu.com"
611 name = "Dynu"
612 website = "http://dynu.com/"
613 protocols = ("ipv6", "ipv4",)
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
621 # DynU sends the IPv6 and IPv4 address in one request
622
623 def update(self):
624 data = DDNSProtocolDynDNS2.prepare_request_data(self, "ipv4")
625
626 # This one supports IPv6
627 myipv6 = self.get_address("ipv6")
628
629 # Add update information if we have an IPv6 address.
630 if myipv6:
631 data["myipv6"] = myipv6
632
633 self._send_request(data)
634
635
636 class DDNSProviderEasyDNS(DDNSProtocolDynDNS2, DDNSProvider):
637 handle = "easydns.com"
638 name = "EasyDNS"
639 website = "http://www.easydns.com/"
640 protocols = ("ipv4",)
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
649 class 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
660 class 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
672 def update_protocol(self, proto):
673 data = {
674 "ip" : self.get_address(proto),
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
704 class DDNSProviderEnomCom(DDNSResponseParserXML, DDNSProvider):
705 handle = "enom.com"
706 name = "eNom Inc."
707 website = "http://www.enom.com/"
708 protocols = ("ipv4",)
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
716 def update_protocol(self, proto):
717 data = {
718 "command" : "setdnshost",
719 "responsetype" : "xml",
720 "address" : self.get_address(proto),
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
747 class 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
757 def update_protocol(self, proto):
758 data = {
759 "ip" : self.get_address(proto),
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:
767 response = self.send_request(url, data=data)
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
787 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
788 handle = "freedns.afraid.org"
789 name = "freedns.afraid.org"
790 website = "http://freedns.afraid.org/"
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
796 def update_protocol(self, proto):
797 data = {
798 "address" : self.get_address(proto),
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
807 # Get the full response message.
808 output = response.read()
809
810 # Handle success messages.
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
820 # If we got here, some other update error happened.
821 raise DDNSUpdateError
822
823
824 class DDNSProviderLightningWireLabs(DDNSProvider):
825 handle = "dns.lightningwirelabs.com"
826 name = "Lightning Wire Labs DNS Service"
827 website = "http://dns.lightningwirelabs.com/"
828
829 # Information about the format of the HTTPS request is to be found
830 # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
831
832 url = "https://dns.lightningwirelabs.com/update"
833
834 def update(self):
835 data = {
836 "hostname" : self.hostname,
837 "address6" : self.get_address("ipv6", "-"),
838 "address4" : self.get_address("ipv4", "-"),
839 }
840
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.
857 response = self.send_request(self.url, data=data)
858
859 # Handle success messages.
860 if response.code == 200:
861 return
862
863 # If we got here, some other update error happened.
864 raise DDNSUpdateError
865
866
867 class DDNSProviderMyOnlinePortal(DDNSProtocolDynDNS2, DDNSProvider):
868 handle = "myonlineportal.net"
869 name = "myonlineportal.net"
870 website = "https:/myonlineportal.net/"
871
872 # Information about the request and response can be obtained here:
873 # https://myonlineportal.net/howto_dyndns
874
875 url = "https://myonlineportal.net/updateddns"
876
877 def prepare_request_data(self, proto):
878 data = {
879 "hostname" : self.hostname,
880 "ip" : self.get_address(proto),
881 }
882
883 return data
884
885
886 class DDNSProviderNamecheap(DDNSResponseParserXML, DDNSProvider):
887 handle = "namecheap.com"
888 name = "Namecheap"
889 website = "http://namecheap.com"
890 protocols = ("ipv4",)
891
892 # Information about the format of the HTTP request is to be found
893 # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
894 # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
895
896 url = "https://dynamicdns.park-your-domain.com/update"
897
898 def update_protocol(self, proto):
899 # Namecheap requires the hostname splitted into a host and domain part.
900 host, domain = self.hostname.split(".", 1)
901
902 data = {
903 "ip" : self.get_address(proto),
904 "password" : self.password,
905 "host" : host,
906 "domain" : domain
907 }
908
909 # Send update to the server.
910 response = self.send_request(self.url, data=data)
911
912 # Get the full response message.
913 output = response.read()
914
915 # Handle success messages.
916 if self.get_xml_tag_value(output, "IP") == address:
917 return
918
919 # Handle error codes.
920 errorcode = self.get_xml_tag_value(output, "ResponseNumber")
921
922 if errorcode == "304156":
923 raise DDNSAuthenticationError
924 elif errorcode == "316153":
925 raise DDNSRequestError(_("Domain not found."))
926 elif errorcode == "316154":
927 raise DDNSRequestError(_("Domain not active."))
928 elif errorcode in ("380098", "380099"):
929 raise DDNSInternalServerError
930
931 # If we got here, some other update error happened.
932 raise DDNSUpdateError
933
934
935 class DDNSProviderNOIP(DDNSProtocolDynDNS2, DDNSProvider):
936 handle = "no-ip.com"
937 name = "No-IP"
938 website = "http://www.no-ip.com/"
939 protocols = ("ipv4",)
940
941 # Information about the format of the HTTP request is to be found
942 # here: http://www.no-ip.com/integrate/request and
943 # here: http://www.no-ip.com/integrate/response
944
945 url = "http://dynupdate.no-ip.com/nic/update"
946
947 def prepare_request_data(self, proto):
948 assert proto == "ipv4"
949
950 data = {
951 "hostname" : self.hostname,
952 "address" : self.get_address(proto),
953 }
954
955 return data
956
957
958 class DDNSProviderNsupdateINFO(DDNSProtocolDynDNS2, DDNSProvider):
959 handle = "nsupdate.info"
960 name = "nsupdate.info"
961 website = "http://nsupdate.info/"
962 protocols = ("ipv6", "ipv4",)
963
964 # Information about the format of the HTTP request can be found
965 # after login on the provider user interface and here:
966 # http://nsupdateinfo.readthedocs.org/en/latest/user.html
967
968 # Nsupdate.info uses the hostname as user part for the HTTP basic auth,
969 # and for the password a so called secret.
970 @property
971 def username(self):
972 return self.get("hostname")
973
974 @property
975 def password(self):
976 return self.token or self.get("secret")
977
978 @property
979 def url(self):
980 # The update URL is different by the used protocol.
981 if self.proto == "ipv4":
982 return "https://ipv4.nsupdate.info/nic/update"
983 elif self.proto == "ipv6":
984 return "https://ipv6.nsupdate.info/nic/update"
985 else:
986 raise DDNSUpdateError(_("Invalid protocol has been given"))
987
988 def prepare_request_data(self, proto):
989 data = {
990 "myip" : self.get_address(proto),
991 }
992
993 return data
994
995
996 class DDNSProviderOpenDNS(DDNSProtocolDynDNS2, DDNSProvider):
997 handle = "opendns.com"
998 name = "OpenDNS"
999 website = "http://www.opendns.com"
1000
1001 # Detailed information about the update request and possible
1002 # response codes can be obtained from here:
1003 # https://support.opendns.com/entries/23891440
1004
1005 url = "https://updates.opendns.com/nic/update"
1006
1007 def prepare_request_data(self, proto):
1008 data = {
1009 "hostname" : self.hostname,
1010 "myip" : self.get_address(proto),
1011 }
1012
1013 return data
1014
1015
1016 class DDNSProviderOVH(DDNSProtocolDynDNS2, DDNSProvider):
1017 handle = "ovh.com"
1018 name = "OVH"
1019 website = "http://www.ovh.com/"
1020 protocols = ("ipv4",)
1021
1022 # OVH only provides very limited information about how to
1023 # update a DynDNS host. They only provide the update url
1024 # on the their german subpage.
1025 #
1026 # http://hilfe.ovh.de/DomainDynHost
1027
1028 url = "https://www.ovh.com/nic/update"
1029
1030 def prepare_request_data(self, proto):
1031 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1032 data.update({
1033 "system" : "dyndns",
1034 })
1035
1036 return data
1037
1038
1039 class DDNSProviderRegfish(DDNSProvider):
1040 handle = "regfish.com"
1041 name = "Regfish GmbH"
1042 website = "http://www.regfish.com/"
1043
1044 # A full documentation to the providers api can be found here
1045 # but is only available in german.
1046 # https://www.regfish.de/domains/dyndns/dokumentation
1047
1048 url = "https://dyndns.regfish.de/"
1049
1050 def update(self):
1051 data = {
1052 "fqdn" : self.hostname,
1053 }
1054
1055 # Check if we update an IPv6 address.
1056 address6 = self.get_address("ipv6")
1057 if address6:
1058 data["ipv6"] = address6
1059
1060 # Check if we update an IPv4 address.
1061 address4 = self.get_address("ipv4")
1062 if address4:
1063 data["ipv4"] = address4
1064
1065 # Raise an error if none address is given.
1066 if not data.has_key("ipv6") and not data.has_key("ipv4"):
1067 raise DDNSConfigurationError
1068
1069 # Check if a token has been set.
1070 if self.token:
1071 data["token"] = self.token
1072
1073 # Raise an error if no token and no useranem and password
1074 # are given.
1075 elif not self.username and not self.password:
1076 raise DDNSConfigurationError(_("No Auth details specified."))
1077
1078 # HTTP Basic Auth is only allowed if no token is used.
1079 if self.token:
1080 # Send update to the server.
1081 response = self.send_request(self.url, data=data)
1082 else:
1083 # Send update to the server.
1084 response = self.send_request(self.url, username=self.username, password=self.password,
1085 data=data)
1086
1087 # Get the full response message.
1088 output = response.read()
1089
1090 # Handle success messages.
1091 if "100" in output or "101" in output:
1092 return
1093
1094 # Handle error codes.
1095 if "401" or "402" in output:
1096 raise DDNSAuthenticationError
1097 elif "408" in output:
1098 raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
1099 elif "409" in output:
1100 raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
1101 elif "412" in output:
1102 raise DDNSRequestError(_("No valid FQDN was given."))
1103 elif "414" in output:
1104 raise DDNSInternalServerError
1105
1106 # If we got here, some other update error happened.
1107 raise DDNSUpdateError
1108
1109
1110 class DDNSProviderSelfhost(DDNSProtocolDynDNS2, DDNSProvider):
1111 handle = "selfhost.de"
1112 name = "Selfhost.de"
1113 website = "http://www.selfhost.de/"
1114 protocols = ("ipv4",)
1115
1116 url = "https://carol.selfhost.de/nic/update"
1117
1118 def prepare_request_data(self, proto):
1119 data = DDNSProtocolDynDNS2.prepare_request_data(self, proto)
1120 data.update({
1121 "hostname" : "1",
1122 })
1123
1124 return data
1125
1126
1127 class DDNSProviderSPDNS(DDNSProtocolDynDNS2, DDNSProvider):
1128 handle = "spdns.org"
1129 name = "SPDNS"
1130 website = "http://spdns.org/"
1131
1132 # Detailed information about request and response codes are provided
1133 # by the vendor. They are using almost the same mechanism and status
1134 # codes as dyndns.org so we can inherit all those stuff.
1135 #
1136 # http://wiki.securepoint.de/index.php/SPDNS_FAQ
1137 # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
1138
1139 url = "https://update.spdns.de/nic/update"
1140
1141 @property
1142 def username(self):
1143 return self.get("username") or self.hostname
1144
1145 @property
1146 def password(self):
1147 return self.get("username") or self.token
1148
1149
1150 class DDNSProviderStrato(DDNSProtocolDynDNS2, DDNSProvider):
1151 handle = "strato.com"
1152 name = "Strato AG"
1153 website = "http:/www.strato.com/"
1154 protocols = ("ipv4",)
1155
1156 # Information about the request and response can be obtained here:
1157 # http://www.strato-faq.de/article/671/So-einfach-richten-Sie-DynDNS-f%C3%BCr-Ihre-Domains-ein.html
1158
1159 url = "https://dyndns.strato.com/nic/update"
1160
1161
1162 class DDNSProviderTwoDNS(DDNSProtocolDynDNS2, DDNSProvider):
1163 handle = "twodns.de"
1164 name = "TwoDNS"
1165 website = "http://www.twodns.de"
1166 protocols = ("ipv4",)
1167
1168 # Detailed information about the request can be found here
1169 # http://twodns.de/en/faqs
1170 # http://twodns.de/en/api
1171
1172 url = "https://update.twodns.de/update"
1173
1174 def prepare_request_data(self, proto):
1175 assert proto == "ipv4"
1176
1177 data = {
1178 "ip" : self.get_address(proto),
1179 "hostname" : self.hostname
1180 }
1181
1182 return data
1183
1184
1185 class DDNSProviderUdmedia(DDNSProtocolDynDNS2, DDNSProvider):
1186 handle = "udmedia.de"
1187 name = "Udmedia GmbH"
1188 website = "http://www.udmedia.de"
1189 protocols = ("ipv4",)
1190
1191 # Information about the request can be found here
1192 # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
1193
1194 url = "https://www.udmedia.de/nic/update"
1195
1196
1197 class DDNSProviderVariomedia(DDNSProtocolDynDNS2, DDNSProvider):
1198 handle = "variomedia.de"
1199 name = "Variomedia"
1200 website = "http://www.variomedia.de/"
1201 protocols = ("ipv6", "ipv4",)
1202
1203 # Detailed information about the request can be found here
1204 # https://dyndns.variomedia.de/
1205
1206 url = "https://dyndns.variomedia.de/nic/update"
1207
1208 def prepare_request_data(self, proto):
1209 data = {
1210 "hostname" : self.hostname,
1211 "myip" : self.get_address(proto),
1212 }
1213
1214 return data
1215
1216
1217 class DDNSProviderZoneedit(DDNSProtocolDynDNS2, DDNSProvider):
1218 handle = "zoneedit.com"
1219 name = "Zoneedit"
1220 website = "http://www.zoneedit.com"
1221 protocols = ("ipv4",)
1222
1223 # Detailed information about the request and the response codes can be
1224 # obtained here:
1225 # http://www.zoneedit.com/doc/api/other.html
1226 # http://www.zoneedit.com/faq.html
1227
1228 url = "https://dynamic.zoneedit.com/auth/dynamic.html"
1229
1230 def update_protocol(self, proto):
1231 data = {
1232 "dnsto" : self.get_address(proto),
1233 "host" : self.hostname
1234 }
1235
1236 # Send update to the server.
1237 response = self.send_request(self.url, username=self.username, password=self.password,
1238 data=data)
1239
1240 # Get the full response message.
1241 output = response.read()
1242
1243 # Handle success messages.
1244 if output.startswith("<SUCCESS"):
1245 return
1246
1247 # Handle error codes.
1248 if output.startswith("invalid login"):
1249 raise DDNSAuthenticationError
1250 elif output.startswith("<ERROR CODE=\"704\""):
1251 raise DDNSRequestError(_("No valid FQDN was given."))
1252 elif output.startswith("<ERROR CODE=\"702\""):
1253 raise DDNSInternalServerError
1254
1255 # If we got here, some other update error happened.
1256 raise DDNSUpdateError
1257
1258
1259 class DDNSProviderZZZZ(DDNSProvider):
1260 handle = "zzzz.io"
1261 name = "zzzz"
1262 website = "https://zzzz.io"
1263 protocols = ("ipv6", "ipv4",)
1264
1265 # Detailed information about the update request can be found here:
1266 # https://zzzz.io/faq/
1267
1268 # Details about the possible response codes have been provided in the bugtracker:
1269 # https://bugzilla.ipfire.org/show_bug.cgi?id=10584#c2
1270
1271 url = "https://zzzz.io/api/v1/update"
1272
1273 def update_protocol(self, proto):
1274 data = {
1275 "ip" : self.get_address(proto),
1276 "token" : self.token,
1277 }
1278
1279 if proto == "ipv6":
1280 data["type"] = "aaaa"
1281
1282 # zzzz uses the host from the full hostname as part
1283 # of the update url.
1284 host, domain = self.hostname.split(".", 1)
1285
1286 # Add host value to the update url.
1287 url = "%s/%s" % (self.url, host)
1288
1289 # Send update to the server.
1290 try:
1291 response = self.send_request(url, data=data)
1292
1293 # Handle error codes.
1294 except DDNSNotFound:
1295 raise DDNSRequestError(_("Invalid hostname specified"))
1296
1297 # Handle success messages.
1298 if response.code == 200:
1299 return
1300
1301 # If we got here, some other update error happened.
1302 raise DDNSUpdateError