Add Udmedia as new provider.
[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 logging
23 import urllib2
24 import xml.dom.minidom
25
26 from i18n import _
27
28 # Import all possible exception types.
29 from .errors import *
30
31 logger = logging.getLogger("ddns.providers")
32 logger.propagate = 1
33
34 class DDNSProvider(object):
35         INFO = {
36                 # A short string that uniquely identifies
37                 # this provider.
38                 "handle"    : None,
39
40                 # The full name of the provider.
41                 "name"      : None,
42
43                 # A weburl to the homepage of the provider.
44                 # (Where to register a new account?)
45                 "website"   : None,
46
47                 # A list of supported protocols.
48                 "protocols" : ["ipv6", "ipv4"],
49         }
50
51         DEFAULT_SETTINGS = {}
52
53         def __init__(self, core, **settings):
54                 self.core = core
55
56                 # Copy a set of default settings and
57                 # update them by those from the configuration file.
58                 self.settings = self.DEFAULT_SETTINGS.copy()
59                 self.settings.update(settings)
60
61         def __repr__(self):
62                 return "<DDNS Provider %s (%s)>" % (self.name, self.handle)
63
64         def __cmp__(self, other):
65                 return cmp(self.hostname, other.hostname)
66
67         @property
68         def name(self):
69                 """
70                         Returns the name of the provider.
71                 """
72                 return self.INFO.get("name")
73
74         @property
75         def website(self):
76                 """
77                         Returns the website URL of the provider
78                         or None if that is not available.
79                 """
80                 return self.INFO.get("website", None)
81
82         @property
83         def handle(self):
84                 """
85                         Returns the handle of this provider.
86                 """
87                 return self.INFO.get("handle")
88
89         def get(self, key, default=None):
90                 """
91                         Get a setting from the settings dictionary.
92                 """
93                 return self.settings.get(key, default)
94
95         @property
96         def hostname(self):
97                 """
98                         Fast access to the hostname.
99                 """
100                 return self.get("hostname")
101
102         @property
103         def username(self):
104                 """
105                         Fast access to the username.
106                 """
107                 return self.get("username")
108
109         @property
110         def password(self):
111                 """
112                         Fast access to the password.
113                 """
114                 return self.get("password")
115
116         @property
117         def protocols(self):
118                 return self.INFO.get("protocols")
119
120         @property
121         def token(self):
122                 """
123                         Fast access to the token.
124                 """
125                 return self.get("token")
126
127         def __call__(self, force=False):
128                 if force:
129                         logger.info(_("Updating %s forced") % self.hostname)
130
131                 # Check if we actually need to update this host.
132                 elif self.is_uptodate(self.protocols):
133                         logger.info(_("%s is already up to date") % self.hostname)
134                         return
135
136                 # Execute the update.
137                 self.update()
138
139         def update(self):
140                 raise NotImplementedError
141
142         def is_uptodate(self, protos):
143                 """
144                         Returns True if this host is already up to date
145                         and does not need to change the IP address on the
146                         name server.
147                 """
148                 for proto in protos:
149                         addresses = self.core.system.resolve(self.hostname, proto)
150
151                         current_address = self.get_address(proto)
152
153                         if not current_address in addresses:
154                                 return False
155
156                 return True
157
158         def send_request(self, *args, **kwargs):
159                 """
160                         Proxy connection to the send request
161                         method.
162                 """
163                 return self.core.system.send_request(*args, **kwargs)
164
165         def get_address(self, proto):
166                 """
167                         Proxy method to get the current IP address.
168                 """
169                 return self.core.system.get_address(proto)
170
171
172 class DDNSProviderAllInkl(DDNSProvider):
173         INFO = {
174                 "handle"    : "all-inkl.com",
175                 "name"      : "All-inkl.com",
176                 "website"   : "http://all-inkl.com/",
177                 "protocols" : ["ipv4",]
178         }
179
180         # There are only information provided by the vendor how to
181         # perform an update on a FRITZ Box. Grab requried informations
182         # from the net.
183         # http://all-inkl.goetze.it/v01/ddns-mit-einfachen-mitteln/
184
185         url = "http://dyndns.kasserver.com"
186
187         def update(self):
188
189                 # There is no additional data required so we directly can
190                 # send our request.
191                 try:
192                         # Send request to the server.
193                         response = self.send_request(self.url, username=self.username, password=self.password)
194
195                         # Handle 401 HTTP Header (Authentication Error)
196                 except urllib2.HTTPError, e:
197                         if e.code == 401:
198                                 raise DDNSAuthenticationError
199
200                         raise
201
202                 # Get the full response message.
203                 output = response.read()
204
205                 # Handle success messages.
206                 if output.startswith("good") or output.startswith("nochg"):
207                         return
208
209                 # If we got here, some other update error happened.
210                 raise DDNSUpdateError
211
212
213 class DDNSProviderDHS(DDNSProvider):
214         INFO = {
215                 "handle"    : "dhs.org",
216                 "name"      : "DHS International",
217                 "website"   : "http://dhs.org/",
218                 "protocols" : ["ipv4",]
219         }
220
221         # No information about the used update api provided on webpage,
222         # grabed from source code of ez-ipudate.
223         url = "http://members.dhs.org/nic/hosts"
224
225         def update(self):
226                 data = {
227                         "domain"       : self.hostname,
228                         "ip"           : self.get_address("ipv4"),
229                         "hostcmd"      : "edit",
230                         "hostcmdstage" : "2",
231                         "type"         : "4",
232                 }
233
234                 # Send update to the server.
235                 response = self.send_request(self.url, username=self.username, password=self.password,
236                         data=data)
237
238                 # Handle success messages.
239                 if response.code == 200:
240                         return
241
242                 # Handle error codes.
243                 elif response.code == 401:
244                         raise DDNSAuthenticationError
245
246                 # If we got here, some other update error happened.
247                 raise DDNSUpdateError
248
249
250 class DDNSProviderDNSpark(DDNSProvider):
251         INFO = {
252                 "handle"    : "dnspark.com",
253                 "name"      : "DNS Park",
254                 "website"   : "http://dnspark.com/",
255                 "protocols" : ["ipv4",]
256         }
257
258         # Informations to the used api can be found here:
259         # https://dnspark.zendesk.com/entries/31229348-Dynamic-DNS-API-Documentation
260         url = "https://control.dnspark.com/api/dynamic/update.php"
261
262         def update(self):
263                 data = {
264                         "domain" : self.hostname,
265                         "ip"     : self.get_address("ipv4"),
266                 }
267
268                 # Send update to the server.
269                 response = self.send_request(self.url, username=self.username, password=self.password,
270                         data=data)
271
272                 # Get the full response message.
273                 output = response.read()
274
275                 # Handle success messages.
276                 if output.startswith("ok") or output.startswith("nochange"):
277                         return
278
279                 # Handle error codes.
280                 if output == "unauth":
281                         raise DDNSAuthenticationError
282                 elif output == "abuse":
283                         raise DDNSAbuseError
284                 elif output == "blocked":
285                         raise DDNSBlockedError
286                 elif output == "nofqdn":
287                         raise DDNSRequestError(_("No valid FQDN was given."))
288                 elif output == "nohost":
289                         raise DDNSRequestError(_("Invalid hostname specified."))
290                 elif output == "notdyn":
291                         raise DDNSRequestError(_("Hostname not marked as a dynamic host."))
292                 elif output == "invalid":
293                         raise DDNSRequestError(_("Invalid IP address has been sent."))
294
295                 # If we got here, some other update error happened.
296                 raise DDNSUpdateError
297
298
299 class DDNSProviderDtDNS(DDNSProvider):
300         INFO = {
301                 "handle"    : "dtdns.com",
302                 "name"      : "DtDNS",
303                 "website"   : "http://dtdns.com/",
304                 "protocols" : ["ipv4",]
305                 }
306
307         # Information about the format of the HTTPS request is to be found
308         # http://www.dtdns.com/dtsite/updatespec
309         url = "https://www.dtdns.com/api/autodns.cfm"
310
311         def update(self):
312                 data = {
313                         "ip" : self.get_address("ipv4"),
314                         "id" : self.hostname,
315                         "pw" : self.password
316                 }
317
318                 # Send update to the server.
319                 response = self.send_request(self.url, data=data)
320
321                 # Get the full response message.
322                 output = response.read()
323
324                 # Remove all leading and trailing whitespace.
325                 output = output.strip()
326
327                 # Handle success messages.
328                 if "now points to" in output:
329                         return
330
331                 # Handle error codes.
332                 if output == "No hostname to update was supplied.":
333                         raise DDNSRequestError(_("No hostname specified."))
334
335                 elif output == "The hostname you supplied is not valid.":
336                         raise DDNSRequestError(_("Invalid hostname specified."))
337
338                 elif output == "The password you supplied is not valid.":
339                         raise DDNSAuthenticationError
340
341                 elif output == "Administration has disabled this account.":
342                         raise DDNSRequestError(_("Account has been disabled."))
343
344                 elif output == "Illegal character in IP.":
345                         raise DDNSRequestError(_("Invalid IP address has been sent."))
346
347                 elif output == "Too many failed requests.":
348                         raise DDNSRequestError(_("Too many failed requests."))
349
350                 # If we got here, some other update error happened.
351                 raise DDNSUpdateError
352
353
354 class DDNSProviderDynDNS(DDNSProvider):
355         INFO = {
356                 "handle"    : "dyndns.org",
357                 "name"      : "Dyn",
358                 "website"   : "http://dyn.com/dns/",
359                 "protocols" : ["ipv4",]
360         }
361
362         # Information about the format of the request is to be found
363         # http://http://dyn.com/support/developers/api/perform-update/
364         # http://dyn.com/support/developers/api/return-codes/
365         url = "https://members.dyndns.org/nic/update"
366
367         def _prepare_request_data(self):
368                 data = {
369                         "hostname" : self.hostname,
370                         "myip"     : self.get_address("ipv4"),
371                 }
372
373                 return data
374
375         def update(self):
376                 data = self._prepare_request_data()
377
378                 # Send update to the server.
379                 response = self.send_request(self.url, data=data,
380                         username=self.username, password=self.password)
381
382                 # Get the full response message.
383                 output = response.read()
384
385                 # Handle success messages.
386                 if output.startswith("good") or output.startswith("nochg"):
387                         return
388
389                 # Handle error codes.
390                 if output == "badauth":
391                         raise DDNSAuthenticationError
392                 elif output == "aduse":
393                         raise DDNSAbuseError
394                 elif output == "notfqdn":
395                         raise DDNSRequestError(_("No valid FQDN was given."))
396                 elif output == "nohost":
397                         raise DDNSRequestError(_("Specified host does not exist."))
398                 elif output == "911":
399                         raise DDNSInternalServerError
400                 elif output == "dnserr":
401                         raise DDNSInternalServerError(_("DNS error encountered."))
402
403                 # If we got here, some other update error happened.
404                 raise DDNSUpdateError
405
406
407 class DDNSProviderDynU(DDNSProviderDynDNS):
408         INFO = {
409                 "handle"    : "dynu.com",
410                 "name"      : "Dynu",
411                 "website"   : "http://dynu.com/",
412                 "protocols" : ["ipv6", "ipv4",]
413         }
414
415
416         # Detailed information about the request and response codes
417         # are available on the providers webpage.
418         # http://dynu.com/Default.aspx?page=dnsapi
419
420         url = "https://api.dynu.com/nic/update"
421
422         def _prepare_request_data(self):
423                 data = DDNSProviderDynDNS._prepare_request_data(self)
424
425                 # This one supports IPv6
426                 data.update({
427                         "myipv6"   : self.get_address("ipv6"),
428                 })
429
430                 return data
431
432
433 class DDNSProviderEasyDNS(DDNSProviderDynDNS):
434         INFO = {
435                 "handle"    : "easydns.com",
436                 "name"      : "EasyDNS",
437                 "website"   : "http://www.easydns.com/",
438                 "protocols" : ["ipv4",]
439         }
440
441         # There is only some basic documentation provided by the vendor,
442         # also searching the web gain very poor results.
443         # http://mediawiki.easydns.com/index.php/Dynamic_DNS
444
445         url = "http://api.cp.easydns.com/dyn/tomato.php"
446
447
448 class DDNSProviderFreeDNSAfraidOrg(DDNSProvider):
449         INFO = {
450                 "handle"    : "freedns.afraid.org",
451                 "name"      : "freedns.afraid.org",
452                 "website"   : "http://freedns.afraid.org/",
453                 "protocols" : ["ipv6", "ipv4",]
454                 }
455
456         # No information about the request or response could be found on the vendor
457         # page. All used values have been collected by testing.
458         url = "https://freedns.afraid.org/dynamic/update.php"
459
460         @property
461         def proto(self):
462                 return self.get("proto")
463
464         def update(self):
465                 address = self.get_address(self.proto)
466
467                 data = {
468                         "address" : address,
469                 }
470
471                 # Add auth token to the update url.
472                 url = "%s?%s" % (self.url, self.token)
473
474                 # Send update to the server.
475                 response = self.send_request(url, data=data)
476
477                 if output.startswith("Updated") or "has not changed" in output:
478                         return
479
480                 # Handle error codes.
481                 if output == "ERROR: Unable to locate this record":
482                         raise DDNSAuthenticationError
483                 elif "is an invalid IP address" in output:
484                         raise DDNSRequestError(_("Invalid IP address has been sent."))
485
486
487 class DDNSProviderLightningWireLabs(DDNSProvider):
488         INFO = {
489                 "handle"    : "dns.lightningwirelabs.com",
490                 "name"      : "Lightning Wire Labs",
491                 "website"   : "http://dns.lightningwirelabs.com/",
492                 "protocols" : ["ipv6", "ipv4",]
493         }
494
495         # Information about the format of the HTTPS request is to be found
496         # https://dns.lightningwirelabs.com/knowledge-base/api/ddns
497         url = "https://dns.lightningwirelabs.com/update"
498
499         def update(self):
500                 data =  {
501                         "hostname" : self.hostname,
502                 }
503
504                 # Check if we update an IPv6 address.
505                 address6 = self.get_address("ipv6")
506                 if address6:
507                         data["address6"] = address6
508
509                 # Check if we update an IPv4 address.
510                 address4 = self.get_address("ipv4")
511                 if address4:
512                         data["address4"] = address4
513
514                 # Raise an error if none address is given.
515                 if not data.has_key("address6") and not data.has_key("address4"):
516                         raise DDNSConfigurationError
517
518                 # Check if a token has been set.
519                 if self.token:
520                         data["token"] = self.token
521
522                 # Check for username and password.
523                 elif self.username and self.password:
524                         data.update({
525                                 "username" : self.username,
526                                 "password" : self.password,
527                         })
528
529                 # Raise an error if no auth details are given.
530                 else:
531                         raise DDNSConfigurationError
532
533                 # Send update to the server.
534                 response = self.send_request(self.url, data=data)
535
536                 # Handle success messages.
537                 if response.code == 200:
538                         return
539
540                 # Handle error codes.
541                 if response.code == 403:
542                         raise DDNSAuthenticationError
543                 elif response.code == 400:
544                         raise DDNSRequestError
545
546                 # If we got here, some other update error happened.
547                 raise DDNSUpdateError
548
549
550 class DDNSProviderNamecheap(DDNSProvider):
551         INFO = {
552                 "handle"    : "namecheap.com",
553                 "name"      : "Namecheap",
554                 "website"   : "http://namecheap.com",
555                 "protocols" : ["ipv4",]
556         }
557
558         # Information about the format of the HTTP request is to be found
559         # https://www.namecheap.com/support/knowledgebase/article.aspx/9249/0/nc-dynamic-dns-to-dyndns-adapter
560         # https://community.namecheap.com/forums/viewtopic.php?f=6&t=6772
561
562         url = "https://dynamicdns.park-your-domain.com/update"
563
564         def parse_xml(self, document, content):
565                 # Send input to the parser.
566                 xmldoc = xml.dom.minidom.parseString(document)
567
568                 # Get XML elements by the given content.
569                 element = xmldoc.getElementsByTagName(content)
570
571                 # If no element has been found, we directly can return None.
572                 if not element:
573                         return None
574
575                 # Only get the first child from an element, even there are more than one.
576                 firstchild = element[0].firstChild
577
578                 # Get the value of the child.
579                 value = firstchild.nodeValue
580
581                 # Return the value.
582                 return value
583                 
584         def update(self):
585                 # Namecheap requires the hostname splitted into a host and domain part.
586                 host, domain = self.hostname.split(".", 1)
587
588                 data = {
589                         "ip"       : self.get_address("ipv4"),
590                         "password" : self.password,
591                         "host"     : host,
592                         "domain"   : domain
593                 }
594
595                 # Send update to the server.
596                 response = self.send_request(self.url, data=data)
597
598                 # Get the full response message.
599                 output = response.read()
600
601                 # Handle success messages.
602                 if self.parse_xml(output, "IP") == self.get_address("ipv4"):
603                         return
604
605                 # Handle error codes.
606                 errorcode = self.parse_xml(output, "ResponseNumber")
607
608                 if errorcode == "304156":
609                         raise DDNSAuthenticationError
610                 elif errorcode == "316153":
611                         raise DDNSRequestError(_("Domain not found."))
612                 elif errorcode == "316154":
613                         raise DDNSRequestError(_("Domain not active."))
614                 elif errorcode in ("380098", "380099"):
615                         raise DDNSInternalServerError
616
617                 # If we got here, some other update error happened.
618                 raise DDNSUpdateError
619
620
621 class DDNSProviderNOIP(DDNSProviderDynDNS):
622         INFO = {
623                 "handle"    : "no-ip.com",
624                 "name"      : "No-IP",
625                 "website"   : "http://www.no-ip.com/",
626                 "protocols" : ["ipv4",]
627         }
628
629         # Information about the format of the HTTP request is to be found
630         # here: http://www.no-ip.com/integrate/request and
631         # here: http://www.no-ip.com/integrate/response
632
633         url = "http://dynupdate.no-ip.com/nic/update"
634
635         def _prepare_request_data(self):
636                 data = {
637                         "hostname" : self.hostname,
638                         "address"  : self.get_address("ipv4"),
639                 }
640
641                 return data
642
643
644 class DDNSProviderOVH(DDNSProviderDynDNS):
645         INFO = {
646                 "handle"    : "ovh.com",
647                 "name"      : "OVH",
648                 "website"   : "http://www.ovh.com/",
649                 "protocols" : ["ipv4",]
650         }
651
652         # OVH only provides very limited information about how to
653         # update a DynDNS host. They only provide the update url
654         # on the their german subpage.
655         #
656         # http://hilfe.ovh.de/DomainDynHost
657
658         url = "https://www.ovh.com/nic/update"
659
660         def _prepare_request_data(self):
661                 data = DDNSProviderDynDNS._prepare_request_data(self)
662                 data.update({
663                         "system" : "dyndns",
664                 })
665
666                 return data
667
668
669 class DDNSProviderRegfish(DDNSProvider):
670         INFO = {
671                 "handle"    : "regfish.com",
672                 "name"      : "Regfish GmbH",
673                 "website"   : "http://www.regfish.com/",
674                 "protocols" : ["ipv6", "ipv4",]
675         }
676
677         # A full documentation to the providers api can be found here
678         # but is only available in german.
679         # https://www.regfish.de/domains/dyndns/dokumentation
680
681         url = "https://dyndns.regfish.de/"
682
683         def update(self):
684                 data = {
685                         "fqdn" : self.hostname,
686                 }
687
688                 # Check if we update an IPv6 address.
689                 address6 = self.get_address("ipv6")
690                 if address6:
691                         data["ipv6"] = address6
692
693                 # Check if we update an IPv4 address.
694                 address4 = self.get_address("ipv4")
695                 if address4:
696                         data["ipv4"] = address4
697
698                 # Raise an error if none address is given.
699                 if not data.has_key("ipv6") and not data.has_key("ipv4"):
700                         raise DDNSConfigurationError
701
702                 # Check if a token has been set.
703                 if self.token:
704                         data["token"] = self.token
705
706                 # Raise an error if no token and no useranem and password
707                 # are given.
708                 elif not self.username and not self.password:
709                         raise DDNSConfigurationError(_("No Auth details specified."))
710
711                 # HTTP Basic Auth is only allowed if no token is used.
712                 if self.token:
713                         # Send update to the server.
714                         response = self.send_request(self.url, data=data)
715                 else:
716                         # Send update to the server.
717                         response = self.send_request(self.url, username=self.username, password=self.password,
718                                 data=data)
719
720                 # Get the full response message.
721                 output = response.read()
722
723                 # Handle success messages.
724                 if "100" in output or "101" in output:
725                         return
726
727                 # Handle error codes.
728                 if "401" or "402" in output:
729                         raise DDNSAuthenticationError
730                 elif "408" in output:
731                         raise DDNSRequestError(_("Invalid IPv4 address has been sent."))
732                 elif "409" in output:
733                         raise DDNSRequestError(_("Invalid IPv6 address has been sent."))
734                 elif "412" in output:
735                         raise DDNSRequestError(_("No valid FQDN was given."))
736                 elif "414" in output:
737                         raise DDNSInternalServerError
738
739                 # If we got here, some other update error happened.
740                 raise DDNSUpdateError
741
742
743 class DDNSProviderSelfhost(DDNSProvider):
744         INFO = {
745                 "handle"    : "selfhost.de",
746                 "name"      : "Selfhost.de",
747                 "website"   : "http://www.selfhost.de/",
748                 "protocols" : ["ipv4",],
749         }
750
751         url = "https://carol.selfhost.de/update"
752
753         def update(self):
754                 data = {
755                         "username" : self.username,
756                         "password" : self.password,
757                         "textmodi" : "1",
758                 }
759
760                 response = self.send_request(self.url, data=data)
761
762                 match = re.search("status=20(0|4)", response.read())
763                 if not match:
764                         raise DDNSUpdateError
765
766
767 class DDNSProviderSPDNS(DDNSProviderDynDNS):
768         INFO = {
769                 "handle"    : "spdns.org",
770                 "name"      : "SPDNS",
771                 "website"   : "http://spdns.org/",
772                 "protocols" : ["ipv4",]
773         }
774
775         # Detailed information about request and response codes are provided
776         # by the vendor. They are using almost the same mechanism and status
777         # codes as dyndns.org so we can inherit all those stuff.
778         #
779         # http://wiki.securepoint.de/index.php/SPDNS_FAQ
780         # http://wiki.securepoint.de/index.php/SPDNS_Update-Tokens
781
782         url = "https://update.spdns.de/nic/update"
783
784
785 class DDNSProviderUdmedia(DDNSProviderDynDNS):
786         INFO = {
787                 "handle"    : "udmedia.de",
788                 "name"      : "Udmedia GmbH",
789                 "website"   : "http://www.udmedia.de",
790                 "protocols" : ["ipv4",]
791         }
792
793         # Information about the request can be found here
794         # http://www.udmedia.de/faq/content/47/288/de/wie-lege-ich-einen-dyndns_eintrag-an.html
795
796         url = "https://www.udmedia.de/nic/update"
797
798
799 class DDNSProviderVariomedia(DDNSProviderDynDNS):
800         INFO = {
801                 "handle"   : "variomedia.de",
802                 "name"     : "Variomedia",
803                 "website"  : "http://www.variomedia.de/",
804                 "protocols" : ["ipv6", "ipv4",]
805         }
806
807         # Detailed information about the request can be found here
808         # https://dyndns.variomedia.de/
809
810         url = "https://dyndns.variomedia.de/nic/update"
811
812         @property
813         def proto(self):
814                 return self.get("proto")
815
816         def _prepare_request_data(self):
817                 data = {
818                         "hostname" : self.hostname,
819                         "myip"     : self.get_address(self.proto)
820                 }
821
822                 return data