"""Bounded JSON transport for fixed, deployment-owned HTTPS endpoints.""" from __future__ import annotations import json import ipaddress from http.client import HTTPException from urllib.error import HTTPError, URLError from urllib.parse import urlsplit from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener class TransportError(Exception): """Deliberately contains no response body, URL, token or request data.""" def fixed_origin(value: str, *, allow_internal_http=False) -> str: parsed = urlsplit(value) internal_http = False if allow_internal_http and parsed.scheme == "http" and parsed.hostname: try: internal_http = ipaddress.ip_address(parsed.hostname).is_loopback except ValueError: internal_http = parsed.hostname.endswith((".svc", ".svc.cluster.local")) if ((parsed.scheme != "https" and not internal_http) or not parsed.hostname or parsed.username or parsed.password or parsed.path or parsed.query or parsed.fragment or any(c.isspace() for c in value)): raise ValueError("a fixed HTTPS origin without path or credentials is required") return value def https_origin(value: str) -> str: return fixed_origin(value) class _NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None class JSONTransport: """No redirects or ambient proxy credentials; five-second request timeout.""" def __init__(self, *, allow_internal_http=False) -> None: self.allow_internal_http = allow_internal_http self._opener = build_opener(ProxyHandler({}), _NoRedirect()) def request(self, method: str, url: str, *, headers=None, body=None) -> tuple[int, dict]: parsed = urlsplit(url) try: fixed_origin(parsed.scheme + "://" + parsed.netloc, allow_internal_http=self.allow_internal_http) except ValueError: raise TransportError("trusted transport required") from None req = Request(url, method=method, data=body, headers=headers or {}) try: try: response = self._opener.open(req, timeout=5) except HTTPError as exc: response = exc with response: status = response.code if 300 <= status < 400: raise TransportError("upstream redirect refused") raw = response.read(262145) if len(raw) > 262144: raise TransportError("upstream response too large") result = json.loads(raw) if not isinstance(result, dict): raise TransportError("upstream object required") return status, result except (URLError, OSError, ValueError, HTTPException): raise TransportError("upstream request failed") from None