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 are pending
ci / test (push) Has started running
Some checks are pending
ci / test (push) Has started running
This commit is contained in:
@@ -1,29 +1,55 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import socket
|
import socket
|
||||||
from urllib.parse import urlsplit
|
from collections.abc import Awaitable, Callable
|
||||||
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
|
Resolver = Callable[[str, int], Awaitable[list[str]]]
|
||||||
|
|
||||||
|
|
||||||
class UnsafeDestination(ValueError):
|
class UnsafeTarget(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def validate_destination(url: str) -> None:
|
def redact_url(url: str) -> str:
|
||||||
parsed = urlsplit(url)
|
parts = urlsplit(url)
|
||||||
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
host = parts.hostname or "invalid"
|
||||||
raise UnsafeDestination("only HTTP(S) URLs with a hostname are allowed")
|
if ":" in host:
|
||||||
if parsed.username or parsed.password:
|
host = f"[{host}]"
|
||||||
raise UnsafeDestination("URL credentials are not allowed")
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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)
|
||||||
try:
|
try:
|
||||||
literal = ipaddress.ip_address(parsed.hostname)
|
literal = ipaddress.ip_address(host)
|
||||||
addresses = [literal]
|
addresses = [str(literal)]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
try:
|
try:
|
||||||
infos = await loop.getaddrinfo(parsed.hostname, parsed.port,
|
addresses = await resolver(host, port)
|
||||||
type=socket.SOCK_STREAM)
|
except (OSError, socket.gaierror) as exc:
|
||||||
except socket.gaierror as exc:
|
raise UnsafeTarget("dns_resolution_failed") from exc
|
||||||
raise UnsafeDestination("destination DNS resolution failed") from exc
|
if not addresses:
|
||||||
addresses = list({ipaddress.ip_address(info[4][0]) for info in infos})
|
raise UnsafeTarget("dns_resolution_failed")
|
||||||
if not addresses or any(not address.is_global for address in addresses):
|
try:
|
||||||
raise UnsafeDestination("destination resolves to a non-public address")
|
if any(not ipaddress.ip_address(address).is_global for address in addresses):
|
||||||
|
raise UnsafeTarget("non_global_target")
|
||||||
|
except ValueError as exc:
|
||||||
|
raise UnsafeTarget("invalid_dns_answer") from exc
|
||||||
|
|||||||
Reference in New Issue
Block a user