diff options
Diffstat (limited to 'gerboweb/deploy/dyndns.py')
-rw-r--r-- | gerboweb/deploy/dyndns.py | 149 |
1 files changed, 0 insertions, 149 deletions
diff --git a/gerboweb/deploy/dyndns.py b/gerboweb/deploy/dyndns.py deleted file mode 100644 index 2546dce..0000000 --- a/gerboweb/deploy/dyndns.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python3 - -import time -from contextlib import contextmanager -import re -import os -import os.path -import random -import string -import subprocess -import sqlite3 -import hmac -from ipaddress import IPv4Address, IPv6Address - -from flask import Flask, request, abort -import uwsgidecorators - -app = Flask(__name__) -app.config.update(dict( - RECORD_EXPIRY_S = 86400, - NSD_CONTROL = 'nsd-control' - )) -app.config.from_pyfile('config.py') - - -ZONEFILE_TEMPLATE = '''\ -; #################################################### ; -; THIS FILE IS AUTOMATICALLY GENERATED! DO NOT MODIFY! ; -; #################################################### ; - -$ORIGIN {zone}. -$TTL 1800 -@ IN SOA {ns}. {mail}. ( - {serial} ; serial number - 60 ; refresh - 60 ; retry - {expire} ; expire - 60 ; ttl - ) -; Name servers - IN NS {ns}. - -; Additional A records from template -; @ IN A 192.0.2.3 -; www IN A 192.0.2.3 - -; Dynamically generated records -{dynamic_records} -''' - -db = sqlite3.connect(app.config['SQLITE_DB'], check_same_thread=False) -with db as conn: - conn.execute('''CREATE TABLE IF NOT EXISTS zone_versions (date TEXT)''') - conn.execute('''CREATE TABLE IF NOT EXISTS records - (name TEXT PRIMARY KEY, ipv4 TEXT, ipv6 TEXT, last_update INTEGER)''') - -def purge_expired_records(): - with db as conn: - conn.execute('DELETE FROM records WHERE last_update < ?', - (int(time.time()) - app.config['RECORD_EXPIRY_S'],)) - -def update_record(record, ipv4=None, ipv6=None): - with db as conn: - old_v4, old_v6 = conn.execute('SELECT ipv4, ipv6 FROM records WHERE name=?', (record,)).fetchone() or (None, None) - conn.execute('INSERT OR REPLACE INTO records VALUES (?, ?, ?, ?)', (record, ipv4, ipv6, int(time.time()))) - return ipv4 != old_v4 or ipv6 != old_v6 - -@contextmanager -def inplace_rewrite(filename, cleanup=True): - print('Writing', filename) - filename = os.path.abspath(filename) - if cleanup: - basename = os.path.basename(filename) - for entry in os.scandir(os.path.dirname(filename)): - if entry.name.startswith(basename) and re.match(r'\.tmp-[a-zA-Z0-9]{8}', entry.name[len(basename):]): - os.remove(entry.path) - - tmp_fn = filename + f'.tmp-' + ''.join(random.choices(string.ascii_letters + string.digits, k=8)) - with open(tmp_fn, 'w') as tmp_f: - yield tmp_f - tmp_f.flush() - os.fsync(tmp_f.fileno()) - os.rename(tmp_fn, filename) - -def write_zonefile(): - # Find the next free zonefile version number - with db as conn: - conn.execute('INSERT INTO zone_versions VALUES (DATE())') - date, version_num, = conn.execute('SELECT zone_versions.date, COUNT(*) FROM zone_versions WHERE zone_versions.date = DATE()').fetchone() - zone_version = f'{date.replace("-", "")}{version_num:02d}' - - # Generate dynamic record block - with db as conn: - records = db.execute('SELECT name, "A", ipv4 FROM records UNION SELECT name, "AAAA", ipv6 FROM records') - dynamic_records = '\n'.join(f'{name:<20} IN {rtype:<4} {value}' for name, rtype, value in records if value is not None) - - # Template zone file content - content = ZONEFILE_TEMPLATE.format( - zone = app.config['ZONE'], - ns = app.config['NAMESERVER'], - mail = app.config['NAMESERVER_MAIL'].replace('@', '.'), - serial = zone_version, - dynamic_records = dynamic_records, - expire = app.config['RECORD_EXPIRY_S'] - ) - - with inplace_rewrite(app.config['ZONEFILE'], cleanup=True) as f: - f.write(content) - -def kick_nsd(): - prog = app.config['NSD_CONTROL'] - if isinstance(prog, str): - prog = [prog] - subprocess.run([*prog, 'reload', app.config['ZONE']], check=True) - -@app.before_first_request -@uwsgidecorators.timer(300) -def update_zonefile(): - purge_expired_records() - write_zonefile() - kick_nsd() - -@app.route('/update', methods=['POST']) -def route_update(): - if request.authorization is None: - abort(403) - - record = request.authorization['username'] - record_config = app.config['DYNAMIC_RECORDS'].get(record) - if record_config is None: - abort(403) - - *supported_formats, password = record_config - if not hmac.compare_digest(request.authorization['password'], password): - abort(403) - - ipv4 = request.args.get('ipv4', '127.0.0.1') - ipv6 = request.args.get('ipv6', '::1') - ipv4 = str(IPv4Address(ipv4)) if 'v4' in supported_formats else None - ipv6 = str(IPv6Address(ipv6)) if 'v6' in supported_formats else None - if update_record(record, ipv4=ipv4, ipv6=ipv6): - update_zonefile() - - return 'success' - - -if __name__ == '__main__': - app.run() - |