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.
Some checks failed
ci / validate (push) Has been cancelled

This commit is contained in:
2026-08-09 15:46:45 +00:00
parent 7b2eaa0dd2
commit 7b63047d64

View File

@@ -2,53 +2,48 @@ import asyncio
import ipaddress import ipaddress
import socket import socket
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from urllib.parse import SplitResult, urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
Resolver = Callable[[str, int], Awaitable[list[str]]] Resolver = Callable[[str, int], Awaitable[list[str]]]
class UnsafeTargetError(ValueError): class DestinationRejected(ValueError):
pass pass
async def resolve_addresses(host: str, port: int) -> list[str]: async def system_resolver(host: str, port: int) -> list[str]:
def lookup() -> list[str]: loop = asyncio.get_running_loop()
records = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM) records = await loop.getaddrinfo(host, port, type=socket.SOCK_STREAM)
return list({record[4][0] for record in records}) return sorted({record[4][0] for record in records})
return await asyncio.to_thread(lookup)
def redacted_url(url: str) -> str: def redacted_url(url: str) -> str:
parsed = urlsplit(url) parts = urlsplit(url)
host = parsed.hostname or "invalid-host" host = parts.hostname or ""
if ":" in host: authority = f"{host}:{parts.port}" if parts.port else host
host = f"[{host}]" query = "<redacted>" if parts.query else ""
port = f":{parsed.port}" if parsed.port else "" return urlunsplit((parts.scheme, authority, parts.path or "/", query, ""))
return urlunsplit((parsed.scheme, f"{host}{port}", parsed.path or "/", "", ""))
async def validate_target(url: str, resolver: Resolver) -> SplitResult: async def require_public_destination(url: str, resolver: Resolver = system_resolver) -> None:
parsed = urlsplit(url) parts = urlsplit(url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname: if parts.scheme not in {"http", "https"} or not parts.hostname:
raise UnsafeTargetError("only absolute HTTP(S) URLs are allowed") raise DestinationRejected("only HTTP(S) destinations are allowed")
if parsed.username is not None or parsed.password is not None: if parts.username is not None or parts.password is not None:
raise UnsafeTargetError("URL credentials are not allowed") raise DestinationRejected("URL user information is not allowed")
try: try:
port = parsed.port or (443 if parsed.scheme == "https" else 80) port = parts.port or (443 if parts.scheme == "https" else 80)
except ValueError as exc: except ValueError as exc:
raise UnsafeTargetError("invalid port") from exc raise DestinationRejected("invalid destination port") from exc
try: try:
addresses = await resolver(parsed.hostname, port) addresses = await resolver(parts.hostname, port)
except (OSError, socket.gaierror) as exc: except (OSError, UnicodeError) as exc:
raise UnsafeTargetError("DNS resolution failed") from exc raise DestinationRejected("destination DNS resolution failed") from exc
if not addresses: if not addresses:
raise UnsafeTargetError("DNS resolution returned no addresses") raise DestinationRejected("destination DNS resolution returned no addresses")
for address in addresses: try:
try: parsed = [ipaddress.ip_address(address) for address in addresses]
ip = ipaddress.ip_address(address) except ValueError as exc:
except ValueError as exc: raise DestinationRejected("destination DNS returned an invalid address") from exc
raise UnsafeTargetError("DNS returned an invalid address") from exc if any(not address.is_global for address in parsed):
if not ip.is_global: raise DestinationRejected("destination resolves to a non-public address")
raise UnsafeTargetError("target resolves to a non-public address")
return parsed