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