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 / container (push) Has been cancelled
ci / quality (push) Has started running

This commit is contained in:
2026-08-09 16:03:44 +00:00
parent 0b1c0be312
commit e96ae3120a

View File

@@ -2,7 +2,7 @@ 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 urlsplit, urlunsplit from urllib.parse import urlsplit
Resolver = Callable[[str, int], Awaitable[list[str]]] Resolver = Callable[[str, int], Awaitable[list[str]]]
@@ -11,45 +11,28 @@ class UnsafeTarget(ValueError):
pass 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]: async def system_resolver(host: str, port: int) -> list[str]:
def resolve() -> list[str]: loop = asyncio.get_running_loop()
rows = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM) records = await loop.getaddrinfo(host, port, type=socket.SOCK_STREAM)
return list({row[4][0] for row in rows}) return sorted({record[4][0] for record in records})
return await asyncio.to_thread(resolve)
async def validate_target(url: str, resolver: Resolver = system_resolver) -> None: async def validate_target(url: str, resolver: Resolver = system_resolver) -> None:
parts = urlsplit(url) parsed = urlsplit(url)
if parts.scheme not in {"http", "https"} or not parts.hostname: if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise UnsafeTarget("invalid_scheme_or_host") raise UnsafeTarget("unsupported_target")
if parts.username is not None or parts.password is not None: if parsed.username or parsed.password:
raise UnsafeTarget("credentials_not_allowed") raise UnsafeTarget("credentials_not_allowed")
host = parts.hostname.rstrip(".").lower() port = parsed.port or (443 if parsed.scheme == "https" else 80)
if host == "localhost" or host.endswith(".localhost"):
raise UnsafeTarget("non_global_target")
port = parts.port or (443 if parts.scheme == "https" else 80)
try: try:
literal = ipaddress.ip_address(host) addresses = await resolver(parsed.hostname, port)
addresses = [str(literal)]
except ValueError:
try:
addresses = await resolver(host, port)
except (OSError, socket.gaierror) as exc: except (OSError, socket.gaierror) as exc:
raise UnsafeTarget("dns_resolution_failed") from exc raise UnsafeTarget("dns_resolution_failed") from exc
if not addresses: if not addresses:
raise UnsafeTarget("dns_resolution_failed") raise UnsafeTarget("dns_resolution_failed")
try: try:
if any(not ipaddress.ip_address(address).is_global for address in addresses): safe = all(ipaddress.ip_address(address).is_global for address in addresses)
raise UnsafeTarget("non_global_target")
except ValueError as exc: 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")