From e96ae3120a0a3e9dbc79254cef8b916e8263a3c8 Mon Sep 17 00:00:00 2001 From: demo-bot Date: Sun, 9 Aug 2026 16:03:44 +0000 Subject: [PATCH] decomposer: generate deliverable files for Define the service contract and project architecture for the FastAPI endpoint monitoring service.; Implement the typed monitor CRUD API and concurrency-safe in-memory state according to the service design.; Implement secure on-demand endpoint checks with status updates, latency measurement, robust error handling, and redacted structured logs.; Add operational API endpoints and environment-driven runtime configuration to the monitoring service.; Create automated tests for the monitoring service.; Package the service with Docker and developer documentation.; Validate the complete project. --- app/security.py | 49 ++++++++++++++++--------------------------------- 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/app/security.py b/app/security.py index a057c81..0b3a61d 100644 --- a/app/security.py +++ b/app/security.py @@ -2,7 +2,7 @@ import asyncio import ipaddress import socket from collections.abc import Awaitable, Callable -from urllib.parse import urlsplit, urlunsplit +from urllib.parse import urlsplit Resolver = Callable[[str, int], Awaitable[list[str]]] @@ -11,45 +11,28 @@ class UnsafeTarget(ValueError): pass -def redact_url(url: str) -> str: - parts = urlsplit(url) - host = parts.hostname or "invalid" - if ":" in host: - host = f"[{host}]" - port = f":{parts.port}" if parts.port else "" - return urlunsplit((parts.scheme, f"{host}{port}", parts.path, "", "")) - - async def system_resolver(host: str, port: int) -> list[str]: - def resolve() -> list[str]: - rows = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM) - return list({row[4][0] for row in rows}) - - return await asyncio.to_thread(resolve) + loop = asyncio.get_running_loop() + records = await loop.getaddrinfo(host, port, type=socket.SOCK_STREAM) + return sorted({record[4][0] for record in records}) async def validate_target(url: str, resolver: Resolver = system_resolver) -> None: - parts = urlsplit(url) - if parts.scheme not in {"http", "https"} or not parts.hostname: - raise UnsafeTarget("invalid_scheme_or_host") - if parts.username is not None or parts.password is not None: + parsed = urlsplit(url) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + raise UnsafeTarget("unsupported_target") + if parsed.username or parsed.password: raise UnsafeTarget("credentials_not_allowed") - host = parts.hostname.rstrip(".").lower() - if host == "localhost" or host.endswith(".localhost"): - raise UnsafeTarget("non_global_target") - port = parts.port or (443 if parts.scheme == "https" else 80) + port = parsed.port or (443 if parsed.scheme == "https" else 80) try: - literal = ipaddress.ip_address(host) - addresses = [str(literal)] - except ValueError: - try: - addresses = await resolver(host, port) - except (OSError, socket.gaierror) as exc: - raise UnsafeTarget("dns_resolution_failed") from exc + addresses = await resolver(parsed.hostname, port) + except (OSError, socket.gaierror) as exc: + raise UnsafeTarget("dns_resolution_failed") from exc if not addresses: raise UnsafeTarget("dns_resolution_failed") try: - if any(not ipaddress.ip_address(address).is_global for address in addresses): - raise UnsafeTarget("non_global_target") + safe = all(ipaddress.ip_address(address).is_global for address in addresses) except ValueError as exc: - raise UnsafeTarget("invalid_dns_answer") from exc + raise UnsafeTarget("dns_resolution_failed") from exc + if not safe: + raise UnsafeTarget("non_public_target")