Refactor source and add SHUTDOWN_TIMEOUT constant

This commit is contained in:
2026-07-22 15:00:28 +02:00
parent 25043be991
commit ca8b29fdc8
+108 -112
View File
@@ -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")