| 1 | #!/usr/bin/python |
|---|
| 2 | # License: GPLv2 |
|---|
| 3 | """Utility for providing a DDNS service for DD-WRT and similar routers using |
|---|
| 4 | Linode's DNS manager. |
|---|
| 5 | """ |
|---|
| 6 | import os |
|---|
| 7 | import ConfigParser as configparser |
|---|
| 8 | |
|---|
| 9 | |
|---|
| 10 | # Define this early so we can use it for import errors. |
|---|
| 11 | def send_response(status, body): |
|---|
| 12 | """Utility function for sending a CGI response""" |
|---|
| 13 | print "Content-type: text/html" |
|---|
| 14 | print status |
|---|
| 15 | print "" |
|---|
| 16 | print "<html>" |
|---|
| 17 | print "<head><title>DDNS update</title></head>" |
|---|
| 18 | print "<body>" |
|---|
| 19 | print body |
|---|
| 20 | print "</body>" |
|---|
| 21 | print "</html>" |
|---|
| 22 | |
|---|
| 23 | |
|---|
| 24 | try: |
|---|
| 25 | from linode.api import Api, ApiError |
|---|
| 26 | except ImportError: |
|---|
| 27 | send_response("Status: 500 Internal Server Error", |
|---|
| 28 | "linode-python package not installed. " |
|---|
| 29 | "See https://pypi.python.org/pypi/linode-python/1.0") |
|---|
| 30 | |
|---|
| 31 | |
|---|
| 32 | def update_domain_resource(fqdn, address, key): |
|---|
| 33 | """Update the Linode DNS manager""" |
|---|
| 34 | api = Api(key=key) |
|---|
| 35 | base_domain = '.'.join(fqdn.split('.')[-2:]) |
|---|
| 36 | hostname = fqdn[:-len(base_domain)-1] |
|---|
| 37 | |
|---|
| 38 | domains_by_name = dict((d['DOMAIN'], d) for d in api.domain_list()) |
|---|
| 39 | domain_to_update = domains_by_name[base_domain] |
|---|
| 40 | domain_id = domain_to_update['DOMAINID'] |
|---|
| 41 | resources = api.domain_resource_list(DomainID=domain_id) |
|---|
| 42 | hosts_by_name = dict((r['NAME'], r) for r in resources) |
|---|
| 43 | host_to_update = hosts_by_name[hostname] |
|---|
| 44 | resource_id = host_to_update['RESOURCEID'] |
|---|
| 45 | # Finally, make the call to update the domain |
|---|
| 46 | api.domain_resource_update(DomainID=domain_id, ResourceID=resource_id, |
|---|
| 47 | Target=address) |
|---|
| 48 | |
|---|
| 49 | |
|---|
| 50 | def cgi_update(): |
|---|
| 51 | """Using the configuration file, update the authenticated user's host names |
|---|
| 52 | to the user's IP address. Returns a CGI status and an explanatory message |
|---|
| 53 | for the body. |
|---|
| 54 | """ |
|---|
| 55 | if os.environ['AUTH_TYPE'] != 'Basic': |
|---|
| 56 | return "Status: 401 Unauthorized", "No authentication given" |
|---|
| 57 | config = configparser.ConfigParser() |
|---|
| 58 | try: |
|---|
| 59 | config.readfp(open(os.path.join(os.path.dirname(__file__), 'ddns.ini'))) |
|---|
| 60 | except IOError, error: |
|---|
| 61 | return ("Status: 500 Internal Server Error", |
|---|
| 62 | "Failed to find configuration file; %s" % error) |
|---|
| 63 | try: |
|---|
| 64 | apikey = config.get('ddns', 'key') |
|---|
| 65 | except configparser.Error, error: |
|---|
| 66 | return ("Status: 500 Internal Server Error", |
|---|
| 67 | "Failed to find key in configuration; %s" % error) |
|---|
| 68 | try: |
|---|
| 69 | user = os.environ['REMOTE_USER'] |
|---|
| 70 | except KeyError: |
|---|
| 71 | return ("Status: 401 Unauthorized", "No user given") |
|---|
| 72 | try: |
|---|
| 73 | new_address = os.environ['REMOTE_ADDR'] |
|---|
| 74 | except KeyError: |
|---|
| 75 | return ("Status: 500 Internal Server Error", "No remote IP address") |
|---|
| 76 | try: |
|---|
| 77 | domains = [d.strip() for d in config.get(user, |
|---|
| 78 | 'domains').split(',')] |
|---|
| 79 | except configparser.Error, error: |
|---|
| 80 | return ("Status: 500 Internal Server Error", |
|---|
| 81 | "Failed to find user's domains in configuration; %s" % error) |
|---|
| 82 | try: |
|---|
| 83 | for domain in domains: |
|---|
| 84 | update_domain_resource(domain, new_address, apikey) |
|---|
| 85 | except ApiError, error: |
|---|
| 86 | return ("Status: 500 Internal Server Error", |
|---|
| 87 | "update of %s failed with %s" % (domain, error)) |
|---|
| 88 | |
|---|
| 89 | return ("Status: 200 OK", |
|---|
| 90 | "Updated %s to %s" % (', '.join(domains), new_address)) |
|---|
| 91 | |
|---|
| 92 | |
|---|
| 93 | def main(): |
|---|
| 94 | """Runs as a CGI script""" |
|---|
| 95 | status, body = cgi_update() |
|---|
| 96 | send_response(status, body) |
|---|
| 97 | return 0 |
|---|
| 98 | |
|---|
| 99 | |
|---|
| 100 | if __name__ == '__main__': |
|---|
| 101 | main() |
|---|