diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | LICENSE | 26 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | ddns.cfg | 30 | ||||
-rw-r--r-- | ddns/__init__.py | 16 | ||||
-rw-r--r-- | ddns/auth.py | 27 | ||||
-rw-r--r-- | ddns/backend/__init__.py | 0 | ||||
-rw-r--r-- | ddns/backend/dnsupdate.py | 74 | ||||
-rw-r--r-- | ddns/cfg_parser.py | 17 | ||||
-rw-r--r-- | ddns/frontend/__init__.py | 0 | ||||
-rw-r--r-- | ddns/frontend/dyn_com.py | 47 | ||||
-rw-r--r-- | ddns/hash.py | 23 | ||||
-rwxr-xr-x | ddns/main.py | 61 | ||||
-rw-r--r-- | main.wsgi | 4 | ||||
-rwxr-xr-x | run.py | 5 |
15 files changed, 344 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc @@ -0,0 +1,26 @@ +Copyright (c) 2014, Marius Halden +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. diff --git a/README.md b/README.md new file mode 100644 index 0000000..62bd795 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +DDNS.py +======= +DDNS.py is a dynamic dns frontend mirroring the dyndns api, to allow +standard dns update clients to be used. + +Dependencies (under Debian) +--------------------------- +* python-dns +* python-flask +* python-yaml +* python-ipy + +// vim: tw=70 diff --git a/ddns.cfg b/ddns.cfg new file mode 100644 index 0000000..8e6fe1c --- /dev/null +++ b/ddns.cfg @@ -0,0 +1,30 @@ +# vim: ai ts=2 sts=2 sw=2 et + +users: + - &user-admin + username: admin + password: password + hash: None + - &user-user1 + username: user1 + password: password + hash: None + +dnskeys: + - &dnskey-test-key + name: test-key + key: key + algorithm: HMAC-MD5 + +zones: + - name: test.example.org. + ns: ns.example.org + key: *dnskey-test-key + domains: + - domain: host1 # host1.test.example.org + users: + - *user-admin + - domain: host2 # host2.test.example.org + users: + - *user-admin + - *user-user1 diff --git a/ddns/__init__.py b/ddns/__init__.py new file mode 100644 index 0000000..b0de21e --- /dev/null +++ b/ddns/__init__.py @@ -0,0 +1,16 @@ +from flask import Flask, request, Response +import ddns.cfg_parser + +cfg_parser.cfg_file = '/home/marius/ddns/ddns.cfg' +cfg_parser.read_config() + +app = Flask(__name__) + +def index(): + return "index" + +app.add_url_rule('/', 'index', index) + +#from ddns.frontend.dyn_com import dyn_com +import ddns.frontend.dyn_com +app.add_url_rule('/nic/update', 'ddns.frontend.dyn_com.dyn_com', ddns.frontend.dyn_com.dyn_com) diff --git a/ddns/auth.py b/ddns/auth.py new file mode 100644 index 0000000..6624aad --- /dev/null +++ b/ddns/auth.py @@ -0,0 +1,27 @@ +## These functions are modified versions of these: http://flask.pocoo.org/snippets/8/ +from flask import request, Response +from functools import wraps +import ddns.cfg_parser +import hash + +auth_cfg = ddns.cfg_parser.cfg['users'] + +def check_auth(username, password): + for user in auth_cfg: + if username == user['username'] and \ + hash.hash(user['hash'], password) == user['password']: + return True + return False + +def authenticate(message='badauth'): + return Response(message, 401, + {'WWW-Authenticate': 'Basic realm="login required"'}) + +def require_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + auth = request.authorization + if not auth or not check_auth(auth.username, auth.password): + return authenticate() + return f(*args, **kwargs) + return decorated diff --git a/ddns/backend/__init__.py b/ddns/backend/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ddns/backend/__init__.py diff --git a/ddns/backend/dnsupdate.py b/ddns/backend/dnsupdate.py new file mode 100644 index 0000000..cd1dee2 --- /dev/null +++ b/ddns/backend/dnsupdate.py @@ -0,0 +1,74 @@ +import dns.query +import dns.tsig +import dns.tsigkeyring +import dns.update +import dns.resolver +import ddns.cfg_parser + +zone_cfg = None +keyring = None + +def resolve(domain, rtype='A'): + return dns.resolver.query(domain, rtype) + +def check_ip(domain, ip, rtype='A'): + ans = resolve(domain.encode('ascii'), rtype) + + if not ans: + return False + + for rdata in ans: + if rdata == ip.strNormal(0): + return True + return False + +def get_zone(name): + for zone in zone_cfg: + if zone['name'] == name: + return zone + return None + +def gen_keyring(dnskeys): + global keyring + + keys = {} + for key in dnskeys: + keys[key['name']] = key['key'] + + keyring = dns.tsigkeyring.from_text(keys) + +def get_hash_method(hash_name): + if hash_name == 'HMAC-MD5': + return dns.tsig.HMAC_MD5 + if hash_name == 'HMAC-SHA1': + return dns.tsig.HMAC_SHA1 + if hash_name == 'HMAC-SHA224': + return dns.tsig.HMAC_SHA224 + if hash_name == 'HMAC-SHA256': + return dns.tsig.HMAC_SHA256 + if hash_name == 'HMAC-SHA384': + return dns.tsig.HMAC_384 + if hash_name == 'HMAC-SHA512': + return dns.tsig.HMAC_512 + return dns.tsig.default_algorithm + +def update_dns(zone, hostname, ip, ttl=300): + zone = get_zone(zone) + dns_srv = zone['ns'] + + update = dns.update.Update(zone['name'], keyring=keyring, \ + keyname=zone['key']['name'], \ + keyalgorithm=get_hash_method(zone['key']['algorithm'])) + + if ip.version() == 6: + rtype = 'AAAA' + else: + rtype = 'A' + +# if not check_ip(hostname+'.'+zone['name'], ip, rtype): + update.replace(hostname.encode('ascii'), ttl, rtype, ip.strNormal(0)) + res = dns.query.tcp(update, dns_srv) + +zone_cfg = ddns.cfg_parser.cfg['zones'] +keyring = gen_keyring(ddns.cfg_parser.cfg['dnskeys']) + diff --git a/ddns/cfg_parser.py b/ddns/cfg_parser.py new file mode 100644 index 0000000..e8134cd --- /dev/null +++ b/ddns/cfg_parser.py @@ -0,0 +1,17 @@ +import yaml +import io +import os + +cfg_file = None +cfg = None + +def read_config(): + global cfg + + if not cfg_file or not os.path.exists(cfg_file): + return None + + with io.open(cfg_file, 'r') as fp: + _cfg = yaml.load(fp) + + cfg = _cfg diff --git a/ddns/frontend/__init__.py b/ddns/frontend/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ddns/frontend/__init__.py diff --git a/ddns/frontend/dyn_com.py b/ddns/frontend/dyn_com.py new file mode 100644 index 0000000..bbd61e5 --- /dev/null +++ b/ddns/frontend/dyn_com.py @@ -0,0 +1,47 @@ +from flask import request, Response +import ddns.backend.dnsupdate +from IPy import IP +import ddns.auth +import ddns.cfg_parser + +@ddns.auth.require_auth +def dyn_com(): + if request.method != 'GET': + return "badagent" + + if not request.args.has_key('hostname'): + return "nohost" + + if len(request.args.getlist('hostname')) > 1: + return "numhost" + + if not request.args.has_key('myip'): + return "nohost" + + hostname = request.args.get('hostname') + if not '.' in hostname: + return "notfqdn" + + zone_name = hostname[hostname.find('.')+1:] + if zone_name[-1] != '.': + zone_name += '.' + + hostname = hostname[0:hostname.find('.')] + + try: + ip = IP(request.args.get('myip')) + except ValueError: + return "nohost" + + for zone in ddns.cfg_parser.cfg['zones']: + if zone_name == zone['name']: + for domain in zone['domains']: + if domain['domain'] == hostname: + for user in domain['users']: + if request.authorization.username == user['username']: + ddns.backend.dnsupdate.update_dns(zone_name, hostname, ip) + # We should probably check something here... + return "good" + return auth.authenticate("!yours") + return "nohost" + return "nohost" diff --git a/ddns/hash.py b/ddns/hash.py new file mode 100644 index 0000000..7bb3b3b --- /dev/null +++ b/ddns/hash.py @@ -0,0 +1,23 @@ +import hashlib + +algs = [None, 'sha1', 'sha256', 'sha512'] + +def hash(algo, passwd): + if algo == None: # None + return passwd + if algo == 'sha1': # sha1 + return sha1(passwd) + if algo == 'sha256': # sha256 + return sha256(passwd) + if algo == 'sha512': # sha512 + return sha512(passwd) + return passwd + +def sha1(passwd): + return hashlib.sha1(passwd).hexdigest() + +def sha256(passwd): + return hashlib.sha256(passwd).hexdigest() + +def sha512(passwd): + return hashlib.sha512(passwd).hexdigest() diff --git a/ddns/main.py b/ddns/main.py new file mode 100755 index 0000000..5ee6eaa --- /dev/null +++ b/ddns/main.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +from flask import Flask, request, Response +import ddns +import cfg_parser +import auth +from IPy import IP + +cfg_file="/srv/http/lden.org/ddns/ddns/ddns.cfg" +cfg = cfg_parser.read_config(cfg_file) + +auth.auth_cfg = cfg['users'] +ddns.zone_cfg = cfg['zones'] +ddns.gen_keyring(cfg['dnskeys']) + +app = Flask(__name__) + +@app.route("/nic/update") +@auth.require_auth +def dyndns(): + if request.method != 'GET': + return "badagent" + + if not request.args.has_key('hostname'): + return "nohost" + + if len(request.args.getlist('hostname')) > 1: + return "numhost" + + if not request.args.has_key('myip'): + return "nohost" + + hostname = request.args.get('hostname') + if not '.' in hostname: + return "notfqdn" + + zone_name = hostname[hostname.find('.')+1:] + if zone_name[-1] != '.': + zone_name += '.' + + hostname = hostname[0:hostname.find('.')] + + try: + ip = IP(request.args.get('myip')) + except ValueError: + return "nohost" + + for zone in cfg['zones']: + if zone_name == zone['name']: + for domain in zone['domains']: + if domain['domain'] == hostname: + for user in domain['users']: + if request.authorization.username == user['username']: + ddns.update_dns(zone_name, hostname, ip) + return "good" + return auth.authenticate("!yours") + return "nohost" + return "nohost" + +if __name__ == "__main__": + app.run(host="0.0.0.0", debug=True) diff --git a/main.wsgi b/main.wsgi new file mode 100644 index 0000000..9e5292c --- /dev/null +++ b/main.wsgi @@ -0,0 +1,4 @@ +#import sys +#sys.path.insert(0, '/path/to/ddns/dir/') +#from main import app as application +from ddns import app as application @@ -0,0 +1,5 @@ +#!/usr/bin/env python +import ddns + +if __name__ == '__main__': + ddns.app.run(host="0.0.0.0", debug=True) |