From ca8b29fdc8089172ac7e2c716e91256a14c47106 Mon Sep 17 00:00:00 2001 From: Thomas Oettli Date: Wed, 22 Jul 2026 15:00:28 +0200 Subject: [PATCH] Refactor source and add SHUTDOWN_TIMEOUT constant --- src/ddns_service/server.py | 220 ++++++++++++++++++------------------- 1 file changed, 108 insertions(+), 112 deletions(-) diff --git a/src/ddns_service/server.py b/src/ddns_service/server.py index 9676f42..6a80a47 100644 --- a/src/ddns_service/server.py +++ b/src/ddns_service/server.py @@ -26,6 +26,7 @@ from .cleanup import ExpiredRecordsCleanupThread, RateLimitCleanupThread from .dns import detect_ip_type from .logging import clear_txn_id, set_txn_id from .models import ( + close_database, DatabaseError, DoesNotExist, EncodingError, @@ -38,6 +39,9 @@ from concurrent.futures import ThreadPoolExecutor from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import parse_qs, urlparse +# Graceful shutdown timeout (seconds) +SHUTDOWN_TIMEOUT = 5 + def extract_param(params, aliases): """Extract first matching param from query params.""" @@ -240,6 +244,99 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): pass return None, None + def _parse_ip_params(self, params, endpoint, client_ip, username, + hostname_param): + """Parse and validate IP address parameters.""" + ipv4 = None + ipv6 = None + + # Process myip parameter + myip = extract_param(params, endpoint["params"]["ipv4"]) + if myip: + try: + rtype, myip = detect_ip_type(myip) + if rtype == "A": + ipv4 = myip + else: + ipv6 = myip + except ValueError: + raise DDNSClientError( + "Bad IP address", 400, STATUS_BADIP, + client=client_ip, username=username, + hostname=hostname_param, ip=myip + ) + + # Process myip6 parameter + myip6 = extract_param(params, endpoint["params"]["ipv6"]) + if myip6: + try: + rtype, myip6 = detect_ip_type(myip6) + if rtype != "AAAA": + raise ValueError + ipv6 = myip6 + except ValueError: + raise DDNSClientError( + "Bad IPv6 address", 400, STATUS_BADIP, + client=client_ip, username=username, + hostname=hostname_param, ipv6=myip6 + ) + + # Auto-detect from client IP if no params + if ipv4 is None and ipv6 is None: + rtype, ip = detect_ip_type(client_ip) + if rtype == "A": + ipv4 = ip + else: + ipv6 = ip + + return ipv4, ipv6 + + def _parse_expiry_ttl(self, params, endpoint, client_ip, username, + hostname_param): + """Parse and validate expiry_ttl parameter.""" + expiry_ttl_param = extract_param(params, endpoint["params"]["expiry_ttl"]) + if not expiry_ttl_param: + return None + + try: + expiry_ttl = int(expiry_ttl_param) + if expiry_ttl < 0: + raise ValueError + except ValueError: + raise DDNSClientError( + "Invalid expiry_ttl", 400, STATUS_NOHOST, + client=client_ip, username=username, + hostname=hostname_param, expiry_ttl=expiry_ttl_param + ) + + # Validate bounds + defaults = self.app.config["defaults"] + + if expiry_ttl == 0: + if not defaults["expiry_ttl_allow_zero"]: + raise DDNSClientError( + "Zero expiry_ttl not allowed", 400, STATUS_NOHOST, + client=client_ip, username=username, + hostname=hostname_param, expiry_ttl=expiry_ttl + ) + else: + ttl_min = defaults["expiry_ttl_min"] + if ttl_min is not None and expiry_ttl < ttl_min: + raise DDNSClientError( + "expiry_ttl below minimum", 400, STATUS_NOHOST, + client=client_ip, username=username, + hostname=hostname_param, expiry_ttl=expiry_ttl, min=ttl_min + ) + ttl_max = defaults["expiry_ttl_max"] + if ttl_max is not None and expiry_ttl > ttl_max: + raise DDNSClientError( + "expiry_ttl above maximum", 400, STATUS_NOHOST, + client=client_ip, username=username, + hostname=hostname_param, expiry_ttl=expiry_ttl, max=ttl_max + ) + + return expiry_ttl + def do_GET(self): """Handle GET requests.""" set_txn_id() @@ -306,7 +403,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): "Auth failed", 401, STATUS_BADAUTH, - client_ip + client_ip=client_ip ) # Process hostname parameter @@ -320,55 +417,9 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): username=username ) - # Process myip parameter - ipv4 = None - myip = extract_param(params, endpoint["params"]["ipv4"]) - if myip: - try: - rtype, myip = detect_ip_type(myip) - if rtype == "A": - ipv4 = myip - else: - ipv6 = myip - except ValueError: - raise DDNSClientError( - "Bad IP address", - 400, - STATUS_BADIP, - client=client_ip, - username=username, - hostname=hostname_param, - ip=myip - ) - - # Process myip6 parameter - ipv6 = None - myip6 = extract_param(params, endpoint["params"]["ipv6"]) - if myip6: - try: - rtype, myip6 = detect_ip_type(myip6) - if rtype == "AAAA": - ipv6 = myip6 - else: - raise ValueError - except ValueError: - raise DDNSClientError( - "Bad IPv6 address", - 400, - STATUS_BADIP, - client=client_ip, - username=username, - hostname=hostname_param, - ipv6=myip6 - ) - - # Auto-detect from client IP if no params - if ipv4 is None and ipv6 is None: - rtype, ip = detect_ip_type(client_ip) - if rtype == "A": - ipv4 = ip - else: - ipv6 = ip + # Parse IP parameters + ipv4, ipv6 = self._parse_ip_params( + params, endpoint, client_ip, username, hostname_param) # Process notify_change parameter notify_change = extract_param( @@ -377,65 +428,9 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): ["1", "y", "yes", "on", "true"] if notify_change else False) - # Process expiry_ttl parameter - expiry_ttl_param = extract_param( - params, endpoint["params"]["expiry_ttl"]) - expiry_ttl = None - if expiry_ttl_param: - try: - expiry_ttl = int(expiry_ttl_param) - if expiry_ttl < 0: - raise ValueError - except ValueError: - raise DDNSClientError( - "Invalid expiry_ttl", - 400, - STATUS_NOHOST, - client=client_ip, - username=username, - hostname=hostname_param, - expiry_ttl=expiry_ttl_param - ) - - # Validate bounds - defaults = self.app.config["defaults"] - - if expiry_ttl == 0: - if not defaults["expiry_ttl_allow_zero"]: - raise DDNSClientError( - "Zero expiry_ttl not allowed", - 400, - STATUS_NOHOST, - client=client_ip, - username=username, - hostname=hostname_param, - expiry_ttl=expiry_ttl - ) - else: - ttl_min = defaults["expiry_ttl_min"] - if ttl_min is not None and expiry_ttl < ttl_min: - raise DDNSClientError( - "expiry_ttl below minimum", - 400, - STATUS_NOHOST, - client=client_ip, - username=username, - hostname=hostname_param, - expiry_ttl=expiry_ttl, - min=ttl_min - ) - ttl_max = defaults["expiry_ttl_max"] - if ttl_max is not None and expiry_ttl > ttl_max: - raise DDNSClientError( - "expiry_ttl above maximum", - 400, - STATUS_NOHOST, - client=client_ip, - username=username, - hostname=hostname_param, - expiry_ttl=expiry_ttl, - max=ttl_max - ) + # Parse expiry_ttl parameter + expiry_ttl = self._parse_expiry_ttl( + params, endpoint, client_ip, username, hostname_param) # Validate credentials user = self._authenticate(client_ip, username, password) @@ -753,12 +748,13 @@ def run_daemon(app): server.handle_request() # Graceful shutdown - wait for active requests - server.wait_for_requests(5) + server.wait_for_requests(SHUTDOWN_TIMEOUT) # Cleanup expired_cleanup_thread.stop() ratelimit_cleanup_thread.stop() - expired_cleanup_thread.join(timeout=5) - ratelimit_cleanup_thread.join(timeout=5) + expired_cleanup_thread.join(timeout=SHUTDOWN_TIMEOUT) + ratelimit_cleanup_thread.join(timeout=SHUTDOWN_TIMEOUT) server.server_close() + close_database() logging.info("Daemon stopped")