1 parent
36180dffd7
commit
ec75ac15b0
30 files changed
+3042
-1
No files matched your search
@@ -0,0 +1,63 @@
|
||||
"""Bounded JSON requests; redirects cannot forward upload credentials."""
|
||||
|
||||
import json
|
||||
from http.client import HTTPException
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
|
||||
|
||||
from . import __version__
|
||||
|
||||
|
||||
class TransportError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoRedirects(HTTPRedirectHandler):
|
||||
def redirect_request(self, request, response, code, message, headers, new_url):
|
||||
return None
|
||||
|
||||
|
||||
def _invalid_constant(value):
|
||||
raise ValueError("Non-finite JSON number")
|
||||
|
||||
|
||||
class JsonHttp:
|
||||
MAX_RESPONSE_BYTES = 2 * 1024 * 1024
|
||||
|
||||
def __init__(self, timeout: float, *, local: bool = False):
|
||||
handlers = [NoRedirects()]
|
||||
if local:
|
||||
# LAN traffic must not go through a workstation's HTTP(S) proxy.
|
||||
handlers.append(ProxyHandler({}))
|
||||
self.opener = build_opener(*handlers)
|
||||
self.timeout = timeout
|
||||
|
||||
def request(self, url: str, *, payload: dict | None = None, token: str | None = None, extra_headers=None) -> dict:
|
||||
headers = {"Accept": "application/json", "User-Agent": f"solar-relay/{__version__}"}
|
||||
headers.update(extra_headers or {})
|
||||
body = None
|
||||
if payload is not None:
|
||||
body = json.dumps(payload, allow_nan=False, separators=(",", ":")).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
request = Request(url, data=body, headers=headers, method="POST" if body is not None else "GET")
|
||||
try:
|
||||
with self.opener.open(request, timeout=self.timeout) as response:
|
||||
raw = response.read(self.MAX_RESPONSE_BYTES + 1)
|
||||
except HTTPError as error:
|
||||
# Do not log server bodies: they can echo request headers or credentials.
|
||||
error.close()
|
||||
raise TransportError(f"HTTP {error.code}") from None
|
||||
except (URLError, OSError, TimeoutError, HTTPException) as error:
|
||||
raise TransportError(f"Network request failed ({type(error).__name__})") from None
|
||||
if len(raw) > self.MAX_RESPONSE_BYTES:
|
||||
raise TransportError("JSON response exceeds 2 MiB")
|
||||
try:
|
||||
value = json.loads(raw, parse_constant=_invalid_constant)
|
||||
except (ValueError, UnicodeError, RecursionError):
|
||||
raise TransportError("Response is not valid JSON") from None
|
||||
if not isinstance(value, dict):
|
||||
raise TransportError("Expected a JSON object")
|
||||
return value
|
||||
|
||||
Reference in new issue
Block a user