diff --git a/src/ddns_service/__init__.py b/src/ddns_service/__init__.py index 1a25a1e..74d7755 100644 --- a/src/ddns_service/__init__.py +++ b/src/ddns_service/__init__.py @@ -10,6 +10,15 @@ import datetime __version__ = "1.0.0" __author__ = "Thomas Oettli " +# DynDNS-compatible response statuses +STATUS_GOOD = "good" +STATUS_NOCHG = "nochg" +STATUS_BADAUTH = "badauth" +STATUS_NOHOST = "nohost" +STATUS_DNSERR = "dnserr" +STATUS_ABUSE = "abuse" +STATUS_BADIP = "badip" + __all__ = [ "app", "cleanup", @@ -23,6 +32,13 @@ __all__ = [ "models", "ratelimit", "server", + "STATUS_GOOD", + "STATUS_NOCHG", + "STATUS_BADAUTH", + "STATUS_NOHOST", + "STATUS_DNSERR", + "STATUS_ABUSE", + "STATUS_BADIP", ] DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S %Z" diff --git a/src/ddns_service/server.py b/src/ddns_service/server.py index a72f126..586d539 100644 --- a/src/ddns_service/server.py +++ b/src/ddns_service/server.py @@ -14,7 +14,17 @@ from urllib.parse import parse_qs, urlparse import argon2 -from . import datetime_str, utc_now +from . import ( + datetime_str, + utc_now, + STATUS_GOOD, + STATUS_NOCHG, + STATUS_BADAUTH, + STATUS_NOHOST, + STATUS_DNSERR, + STATUS_ABUSE, + STATUS_BADIP, +) from .cleanup import ExpiredRecordsCleanupThread, RateLimitCleanupThread from .logging import clear_txn_id, set_txn_id from .models import DoesNotExist, get_hostname_for_user, get_user @@ -196,7 +206,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): logging.warning( f"Rate limited (bad): client={client_ip}, " f"retry_at={datetime_str(retry_at)}") - self.respond(429, "abuse") + self.respond(429, STATUS_ABUSE) return # Parse URL @@ -219,7 +229,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): if not username or not password: logging.warning(f"Auth failed: client={client_ip} user=anonymous") - self._handle_bad_request(client_ip, 401, "badauth") + self._handle_bad_request(client_ip, 401, STATUS_BADAUTH) return # Validate credentials @@ -228,14 +238,14 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): self.app.password_hasher.verify(user.password_hash, password) except (DoesNotExist, argon2.exceptions.VerifyMismatchError): logging.warning(f"Auth failed: client={client_ip} user={username}") - self._handle_bad_request(client_ip, 401, "badauth") + self._handle_bad_request(client_ip, 401, STATUS_BADAUTH) return # Get hostname parameter hostname_param = extract_param(params, endpoint["params"]["hostname"]) if not hostname_param: logging.warning(f"Missing hostname: client={client_ip} user={username}") - self._handle_bad_request(client_ip, 400, "nohost") + self._handle_bad_request(client_ip, 400, STATUS_NOHOST) return # Validate and encode hostname @@ -245,7 +255,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): logging.warning( f"Invalid hostname: client={client_ip}, " f"hostname={hostname_param}") - self._handle_bad_request(client_ip, 400, "nohost") + self._handle_bad_request(client_ip, 400, STATUS_NOHOST) return # Check hostname ownership @@ -256,7 +266,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): f"Access denied: client={client_ip} user={username} " f"hostname={hostname_param}" ) - self._handle_bad_request(client_ip, 403, "nohost") + self._handle_bad_request(client_ip, 403, STATUS_NOHOST) return # Good rate limit check @@ -266,7 +276,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): logging.warning( f"Rate limited: client={client_ip}, " f"retry_at={datetime_str(retry_at)}") - self.respond(429, "abuse") + self.respond(429, STATUS_ABUSE) return # Record good request @@ -305,7 +315,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): else: ipv6 = myip except ValueError: - return (400, "badip") + return (400, STATUS_BADIP) # Process myip6 parameter if myip6: @@ -314,9 +324,9 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): if rtype == "AAAA": ipv6 = myip6 else: - return (400, "badip") + return (400, STATUS_BADIP) except ValueError: - return (400, "badip") + return (400, STATUS_BADIP) # Auto-detect from client IP if no params if ipv4 is None and ipv6 is None: @@ -327,7 +337,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): else: ipv6 = ip except ValueError: - return (400, "badip") + return (400, STATUS_BADIP) now = utc_now() @@ -352,7 +362,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): f"DNS update failed: client={client_ip} hostname={hostname.hostname} " f"zone={hostname.zone} ipv4={ipv4} error={e}" ) - return (500, "dnserr") + return (500, STATUS_DNSERR) if ipv6: hostname.last_ipv6_update = now @@ -373,7 +383,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): f"DNS update failed: client={client_ip} hostname={hostname.hostname} " f"zone={hostname.zone} ipv6={ipv6} error={e}" ) - return (500, "dnserr") + return (500, STATUS_DNSERR) # Update database hostname.save() @@ -394,7 +404,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): f"zone={hostname.zone}{changed_addrs} notify_change={str(notify_change).lower()}" ) return ( - 200, "nochg", + 200, STATUS_NOCHG, {"ipv4": hostname.last_ipv4, "ipv6": hostname.last_ipv6} ) @@ -415,7 +425,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler): logging.error(f"Sending change notification error: {e}") return ( - 200, "good", + 200, STATUS_GOOD, {"ipv4": hostname.last_ipv4, "ipv6": hostname.last_ipv6} )