fix(assessment): establish independent release trust
This commit is contained in:
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
@@ -20,6 +21,9 @@ from govoplan_assessment import render_review, review_capability_fit # noqa: E4
|
||||
from govoplan_release.catalog import DEFAULT_PUBLIC_BASE_URL, fetch_json # noqa: E402
|
||||
|
||||
|
||||
TRUSTED_KEYRING_FILE_ENV = "GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_FILE"
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate a fit assessment and identify conclusions needing review after release drift."
|
||||
@@ -44,7 +48,17 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
"--catalog", type=Path, help="Read a catalog from a local file."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keyring", type=Path, help="Trusted local keyring; required with --catalog."
|
||||
"--keyring",
|
||||
type=Path,
|
||||
help="Published or candidate keyring whose hash is pinned by --catalog.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trusted-keyring",
|
||||
type=Path,
|
||||
help=(
|
||||
"Independently pinned local keyring used as the catalog signature trust "
|
||||
f"root; defaults to ${TRUSTED_KEYRING_FILE_ENV}."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--workspace-root",
|
||||
@@ -74,7 +88,14 @@ def main(argv: list[str] | None = None) -> int:
|
||||
raise SystemExit("--keyring is required with --catalog")
|
||||
if args.public and args.keyring is not None:
|
||||
raise SystemExit("--keyring cannot be combined with --public")
|
||||
|
||||
configured_trusted_keyring = os.getenv(TRUSTED_KEYRING_FILE_ENV, "").strip()
|
||||
trusted_keyring_path = args.trusted_keyring or (
|
||||
Path(configured_trusted_keyring) if configured_trusted_keyring else None
|
||||
)
|
||||
if trusted_keyring_path is None:
|
||||
raise SystemExit(
|
||||
f"--trusted-keyring or ${TRUSTED_KEYRING_FILE_ENV} is required"
|
||||
)
|
||||
assessment = read_object(args.assessment, label="assessment")
|
||||
schema = read_object(args.schema, label="schema")
|
||||
if args.public:
|
||||
@@ -82,18 +103,24 @@ def main(argv: list[str] | None = None) -> int:
|
||||
f"{DEFAULT_PUBLIC_BASE_URL}/catalogs/v1/channels/stable.json",
|
||||
label="catalog",
|
||||
)
|
||||
keyring = fetch_public_json(
|
||||
f"{DEFAULT_PUBLIC_BASE_URL}/catalogs/v1/keyring.json", label="keyring"
|
||||
published_keyring = fetch_public_json(
|
||||
f"{DEFAULT_PUBLIC_BASE_URL}/catalogs/v1/keyring.json",
|
||||
label="published keyring",
|
||||
)
|
||||
else:
|
||||
catalog = read_object(args.catalog, label="catalog")
|
||||
keyring = read_object(args.keyring, label="keyring")
|
||||
published_keyring = read_object(args.keyring, label="published keyring")
|
||||
trusted_keyring = read_object(
|
||||
trusted_keyring_path,
|
||||
label="trusted keyring",
|
||||
)
|
||||
|
||||
report = review_capability_fit(
|
||||
assessment=assessment,
|
||||
schema=schema,
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
published_keyring=published_keyring,
|
||||
trusted_keyring=trusted_keyring,
|
||||
workspace_root=None
|
||||
if args.skip_tag_provenance
|
||||
else args.workspace_root.resolve(),
|
||||
|
||||
@@ -46,7 +46,8 @@ def review_capability_fit(
|
||||
assessment: dict[str, Any],
|
||||
schema: dict[str, Any],
|
||||
catalog: dict[str, Any],
|
||||
keyring: dict[str, Any],
|
||||
published_keyring: dict[str, Any],
|
||||
trusted_keyring: dict[str, Any],
|
||||
workspace_root: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return a secret-free release-drift report for one fit assessment."""
|
||||
@@ -64,10 +65,15 @@ def review_capability_fit(
|
||||
changes=[],
|
||||
review_targets=[],
|
||||
catalog_checked=False,
|
||||
local_tag_provenance_checked=False,
|
||||
local_tag_provenance_attempted=0,
|
||||
local_tag_provenance_expected=0,
|
||||
local_tag_catalog_scope_complete=False,
|
||||
)
|
||||
|
||||
catalog_validation = validate_catalog(catalog=catalog, keyring=keyring)
|
||||
catalog_validation = validate_catalog(
|
||||
catalog=catalog,
|
||||
trusted_keyring=trusted_keyring,
|
||||
)
|
||||
if catalog_validation.get("valid") is not True:
|
||||
findings.append(
|
||||
Finding(
|
||||
@@ -100,13 +106,13 @@ def review_capability_fit(
|
||||
for issue in source_selection.issues
|
||||
)
|
||||
expected_keyring_hash = _catalog_keyring_hash(catalog)
|
||||
actual_keyring_hash = canonical_hash(keyring)
|
||||
actual_keyring_hash = canonical_hash(published_keyring)
|
||||
if expected_keyring_hash is None:
|
||||
findings.append(
|
||||
Finding(
|
||||
"blocker",
|
||||
"catalog_keyring_hash_missing",
|
||||
"Catalog release metadata does not pin the trusted keyring hash.",
|
||||
"Catalog release metadata does not pin the published keyring hash.",
|
||||
("assessment.release",),
|
||||
)
|
||||
)
|
||||
@@ -115,7 +121,7 @@ def review_capability_fit(
|
||||
Finding(
|
||||
"blocker",
|
||||
"catalog_keyring_hash_mismatch",
|
||||
"The supplied keyring does not match the hash pinned by the catalog.",
|
||||
"The published keyring does not match the hash pinned by the catalog.",
|
||||
("assessment.release",),
|
||||
)
|
||||
)
|
||||
@@ -163,13 +169,58 @@ def review_capability_fit(
|
||||
changes: list[dict[str, Any]] = []
|
||||
changed_repositories: set[str] = set()
|
||||
assessed_module_ids: set[str] = set()
|
||||
composition = assessment["composition"]
|
||||
local_tag_provenance_expected = len(composition)
|
||||
local_tag_provenance_attempted = 0
|
||||
local_tag_catalog_scope_complete = True
|
||||
|
||||
for module in assessment["composition"]:
|
||||
for module in composition:
|
||||
module_id = str(module["module_id"])
|
||||
repository = str(module["repository"])
|
||||
assessed_version = str(module["manifest_version"])
|
||||
selected_version = source_selection.selected_versions.get(repository)
|
||||
selected_commit = source_selection.selected_commits.get(repository)
|
||||
selected_tag_object = source_selection.selected_tag_objects.get(repository)
|
||||
assessed_module_ids.add(module_id)
|
||||
if workspace_root is not None:
|
||||
local_tag_provenance_attempted += 1
|
||||
provenance = local_tag_provenance(
|
||||
workspace_root=workspace_root,
|
||||
repository=repository,
|
||||
version=assessed_version,
|
||||
assessed_commit=str(module["commit"]),
|
||||
selected_commit=(
|
||||
selected_commit if selected_version == assessed_version else None
|
||||
),
|
||||
selected_tag_object=(
|
||||
selected_tag_object
|
||||
if selected_version == assessed_version
|
||||
else None
|
||||
),
|
||||
)
|
||||
if provenance is not None:
|
||||
changes.append(
|
||||
{
|
||||
"module_id": module_id,
|
||||
"repository": repository,
|
||||
"kind": provenance["kind"],
|
||||
"assessed_commit": module["commit"],
|
||||
"tag_commit": provenance.get("tag_commit"),
|
||||
"tag_object": provenance.get("tag_object"),
|
||||
}
|
||||
)
|
||||
changed_repositories.add(repository)
|
||||
findings.append(
|
||||
Finding(
|
||||
"review",
|
||||
"tag_provenance_changed",
|
||||
str(provenance["message"]),
|
||||
(f"composition.{module_id}",),
|
||||
)
|
||||
)
|
||||
entry = catalog_entries.get(module_id)
|
||||
if entry is None:
|
||||
local_tag_catalog_scope_complete = False
|
||||
changes.append(
|
||||
{
|
||||
"module_id": module_id,
|
||||
@@ -211,7 +262,6 @@ def review_capability_fit(
|
||||
)
|
||||
)
|
||||
|
||||
assessed_version = str(module["manifest_version"])
|
||||
catalog_version = entry["version"]
|
||||
if assessed_version != catalog_version:
|
||||
changes.append(
|
||||
@@ -233,8 +283,6 @@ def review_capability_fit(
|
||||
)
|
||||
)
|
||||
|
||||
selected_version = source_selection.selected_versions.get(repository)
|
||||
selected_commit = source_selection.selected_commits.get(repository)
|
||||
assessed_commit = str(module["commit"]).lower()
|
||||
if (
|
||||
selected_version == assessed_version
|
||||
@@ -280,33 +328,6 @@ def review_capability_fit(
|
||||
)
|
||||
)
|
||||
|
||||
if workspace_root is not None:
|
||||
provenance = local_tag_provenance(
|
||||
workspace_root=workspace_root,
|
||||
repository=repository,
|
||||
version=assessed_version,
|
||||
assessed_commit=str(module["commit"]),
|
||||
)
|
||||
if provenance is not None:
|
||||
changes.append(
|
||||
{
|
||||
"module_id": module_id,
|
||||
"repository": repository,
|
||||
"kind": provenance["kind"],
|
||||
"assessed_commit": module["commit"],
|
||||
"tag_commit": provenance.get("tag_commit"),
|
||||
}
|
||||
)
|
||||
changed_repositories.add(repository)
|
||||
findings.append(
|
||||
Finding(
|
||||
"review",
|
||||
"tag_provenance_changed",
|
||||
str(provenance["message"]),
|
||||
(f"composition.{module_id}",),
|
||||
)
|
||||
)
|
||||
|
||||
extras = sorted(set(catalog_entries) - assessed_module_ids)
|
||||
if extras:
|
||||
findings.append(
|
||||
@@ -331,7 +352,9 @@ def review_capability_fit(
|
||||
changes=changes,
|
||||
review_targets=review_targets,
|
||||
catalog_checked=True,
|
||||
local_tag_provenance_checked=workspace_root is not None,
|
||||
local_tag_provenance_attempted=local_tag_provenance_attempted,
|
||||
local_tag_provenance_expected=local_tag_provenance_expected,
|
||||
local_tag_catalog_scope_complete=local_tag_catalog_scope_complete,
|
||||
)
|
||||
|
||||
|
||||
@@ -357,9 +380,9 @@ def validate_assessment(
|
||||
|
||||
|
||||
def validate_catalog(
|
||||
*, catalog: dict[str, Any], keyring: dict[str, Any]
|
||||
*, catalog: dict[str, Any], trusted_keyring: dict[str, Any]
|
||||
) -> dict[str, object]:
|
||||
trusted_keys = trusted_keys_from_keyring(keyring)
|
||||
trusted_keys = trusted_keys_from_keyring(trusted_keyring)
|
||||
channel = _text(catalog.get("channel"))
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", encoding="utf-8"
|
||||
@@ -394,11 +417,19 @@ def trusted_keys_from_keyring(payload: dict[str, Any]) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
keys = payload.get("keys")
|
||||
if not isinstance(keys, list):
|
||||
return result
|
||||
return {
|
||||
str(key): str(value)
|
||||
for key, value in payload.items()
|
||||
if str(key).strip() and str(value).strip()
|
||||
}
|
||||
for item in keys:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if str(item.get("status") or "active").strip().lower() != "active":
|
||||
if str(item.get("status") or "active").strip().lower() in {
|
||||
"revoked",
|
||||
"disabled",
|
||||
"retired",
|
||||
}:
|
||||
continue
|
||||
raw_not_before = item.get("not_before")
|
||||
raw_not_after = item.get("not_after")
|
||||
@@ -497,6 +528,8 @@ def local_tag_provenance(
|
||||
repository: str,
|
||||
version: str,
|
||||
assessed_commit: str,
|
||||
selected_commit: str | None = None,
|
||||
selected_tag_object: str | None = None,
|
||||
) -> dict[str, str] | None:
|
||||
if REPOSITORY_ID_PATTERN.fullmatch(repository) is None:
|
||||
return {
|
||||
@@ -535,6 +568,19 @@ def local_tag_provenance(
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
tag_object_result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(repo),
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
f"refs/tags/v{version}",
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except OSError:
|
||||
return {
|
||||
"kind": "tag_provenance_unavailable",
|
||||
@@ -555,13 +601,40 @@ def local_tag_provenance(
|
||||
"kind": "tag_provenance_unavailable",
|
||||
"message": f"Cannot peel {repository} tag v{version} in the local checkout.",
|
||||
}
|
||||
if tag_object_result.returncode != 0:
|
||||
return {
|
||||
"kind": "tag_provenance_unavailable",
|
||||
"message": f"Cannot resolve {repository} tag object v{version} in the local checkout.",
|
||||
}
|
||||
tag_commit = result.stdout.strip().lower()
|
||||
tag_object = tag_object_result.stdout.strip().lower()
|
||||
if not tag_commit.startswith(assessed_commit.lower()):
|
||||
return {
|
||||
"kind": "tag_commit_changed",
|
||||
"message": f"{repository} v{version} peels to {tag_commit[:12]}, not assessed commit {assessed_commit}.",
|
||||
"tag_commit": tag_commit,
|
||||
}
|
||||
if selected_commit is not None and tag_commit != selected_commit.lower():
|
||||
return {
|
||||
"kind": "tag_signed_commit_changed",
|
||||
"message": (
|
||||
f"{repository} v{version} peels to {tag_commit[:12]}, not signed "
|
||||
f"selected-unit commit {selected_commit[:12]}."
|
||||
),
|
||||
"tag_commit": tag_commit,
|
||||
"tag_object": tag_object,
|
||||
}
|
||||
if selected_tag_object is not None and tag_object != selected_tag_object.lower():
|
||||
return {
|
||||
"kind": "tag_object_changed",
|
||||
"message": (
|
||||
f"{repository} v{version} resolves to annotated tag object "
|
||||
f"{tag_object[:12]}, not signed selected-unit tag object "
|
||||
f"{selected_tag_object[:12]}."
|
||||
),
|
||||
"tag_commit": tag_commit,
|
||||
"tag_object": tag_object,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@@ -627,7 +700,9 @@ def build_report(
|
||||
changes: list[dict[str, Any]],
|
||||
review_targets: list[dict[str, Any]],
|
||||
catalog_checked: bool,
|
||||
local_tag_provenance_checked: bool,
|
||||
local_tag_provenance_attempted: int,
|
||||
local_tag_provenance_expected: int,
|
||||
local_tag_catalog_scope_complete: bool,
|
||||
) -> dict[str, Any]:
|
||||
finding_list = sorted(
|
||||
findings,
|
||||
@@ -635,18 +710,24 @@ def build_report(
|
||||
)
|
||||
finding_codes = {item.code for item in finding_list}
|
||||
schema_valid = "assessment_schema" not in finding_codes
|
||||
catalog_valid = catalog_checked and not finding_codes.intersection(
|
||||
trusted_signature_valid = catalog_checked and "catalog_trust" not in finding_codes
|
||||
published_keyring_valid = catalog_checked and not finding_codes.intersection(
|
||||
{
|
||||
"catalog_trust",
|
||||
"catalog_keyring_hash_missing",
|
||||
"catalog_keyring_hash_mismatch",
|
||||
}
|
||||
)
|
||||
catalog_valid = trusted_signature_valid and published_keyring_valid
|
||||
release_valid = catalog_valid and not any(
|
||||
item.severity in {"blocker", "review"} for item in finding_list
|
||||
)
|
||||
tag_provenance_checked = (
|
||||
local_tag_provenance_expected > 0
|
||||
and local_tag_provenance_attempted == local_tag_provenance_expected
|
||||
and local_tag_catalog_scope_complete
|
||||
)
|
||||
tag_provenance_valid = (
|
||||
local_tag_provenance_checked and "tag_provenance_changed" not in finding_codes
|
||||
tag_provenance_checked and "tag_provenance_changed" not in finding_codes
|
||||
)
|
||||
if any(item.severity == "blocker" for item in finding_list):
|
||||
status = "blocked"
|
||||
@@ -672,13 +753,23 @@ def build_report(
|
||||
"checked": catalog_checked,
|
||||
"valid": catalog_valid if catalog_checked else None,
|
||||
},
|
||||
"catalog_signature_and_trusted_keyring": {
|
||||
"checked": catalog_checked,
|
||||
"valid": trusted_signature_valid if catalog_checked else None,
|
||||
},
|
||||
"published_keyring_hash": {
|
||||
"checked": catalog_checked,
|
||||
"valid": published_keyring_valid if catalog_checked else None,
|
||||
},
|
||||
"release_metadata": {
|
||||
"checked": catalog_checked,
|
||||
"valid": release_valid if catalog_checked else None,
|
||||
},
|
||||
"local_tag_provenance": {
|
||||
"checked": local_tag_provenance_checked,
|
||||
"valid": tag_provenance_valid if local_tag_provenance_checked else None,
|
||||
"checked": tag_provenance_checked,
|
||||
"valid": tag_provenance_valid if tag_provenance_checked else None,
|
||||
"attempted_count": local_tag_provenance_attempted,
|
||||
"expected_count": local_tag_provenance_expected,
|
||||
},
|
||||
"installed_artifacts": {"checked": False, "valid": None},
|
||||
"target_environment": {"checked": False, "valid": None},
|
||||
|
||||
@@ -7,11 +7,12 @@ import json
|
||||
from pathlib import Path
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
from .http_fetch import fetch_http
|
||||
from .http_fetch import DEFAULT_MAX_RESPONSE_BYTES, fetch_http
|
||||
from .model import CatalogSnapshot
|
||||
from .workspace import website_root
|
||||
|
||||
DEFAULT_PUBLIC_BASE_URL = "https://govoplan.add-ideas.de"
|
||||
MAX_PUBLIC_JSON_RESPONSE_BYTES = DEFAULT_MAX_RESPONSE_BYTES
|
||||
|
||||
|
||||
def collect_catalog_snapshot(
|
||||
@@ -22,9 +23,13 @@ def collect_catalog_snapshot(
|
||||
check_public: bool = False,
|
||||
) -> CatalogSnapshot:
|
||||
web_root = website_root(workspace_root)
|
||||
catalog_path = web_root / "public" / "catalogs" / "v1" / "channels" / f"{channel}.json"
|
||||
catalog_path = (
|
||||
web_root / "public" / "catalogs" / "v1" / "channels" / f"{channel}.json"
|
||||
)
|
||||
keyring_path = web_root / "public" / "catalogs" / "v1" / "keyring.json"
|
||||
public_catalog_url = f"{public_base_url.rstrip('/')}/catalogs/v1/channels/{channel}.json"
|
||||
public_catalog_url = (
|
||||
f"{public_base_url.rstrip('/')}/catalogs/v1/channels/{channel}.json"
|
||||
)
|
||||
public_keyring_url = f"{public_base_url.rstrip('/')}/catalogs/v1/keyring.json"
|
||||
|
||||
catalog_version: str | None = None
|
||||
@@ -129,8 +134,12 @@ def collect_catalog_snapshot(
|
||||
public_keyring_hash=public_keyring_hash,
|
||||
public_catalog_error=public_catalog_error,
|
||||
public_keyring_error=public_keyring_error,
|
||||
catalog_matches_public=compare_hashes(local_catalog_hash, public_catalog_hash) if check_public else None,
|
||||
keyring_matches_public=compare_hashes(local_keyring_hash, public_keyring_hash) if check_public else None,
|
||||
catalog_matches_public=compare_hashes(local_catalog_hash, public_catalog_hash)
|
||||
if check_public
|
||||
else None,
|
||||
keyring_matches_public=compare_hashes(local_keyring_hash, public_keyring_hash)
|
||||
if check_public
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -149,7 +158,9 @@ def parse_catalog_payload(payload: object) -> dict[str, str | int | None]:
|
||||
signatures = payload.get("signatures")
|
||||
return {
|
||||
"catalog_version": string_or_none(payload.get("catalog_version")),
|
||||
"core_release_version": string_or_none(core_release.get("version")) if isinstance(core_release, dict) else None,
|
||||
"core_release_version": string_or_none(core_release.get("version"))
|
||||
if isinstance(core_release, dict)
|
||||
else None,
|
||||
"module_count": len(modules) if isinstance(modules, list) else 0,
|
||||
"signature_count": len(signatures) if isinstance(signatures, list) else 0,
|
||||
"generated_at": string_or_none(payload.get("generated_at")),
|
||||
@@ -164,6 +175,7 @@ def fetch_json(url: str) -> dict[str, object]:
|
||||
timeout=10,
|
||||
label="Public release catalog URL",
|
||||
headers={"User-Agent": "GovOPlaN release-console"},
|
||||
max_response_bytes=MAX_PUBLIC_JSON_RESPONSE_BYTES,
|
||||
)
|
||||
return {"ok": True, "payload": json.loads(response.text())}
|
||||
except (HTTPError, URLError, TimeoutError, ValueError, json.JSONDecodeError) as exc:
|
||||
@@ -171,7 +183,9 @@ def fetch_json(url: str) -> dict[str, object]:
|
||||
|
||||
|
||||
def canonical_hash(payload: object) -> str:
|
||||
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
|
||||
canonical = json.dumps(
|
||||
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True
|
||||
).encode("utf-8")
|
||||
return hashlib.sha256(canonical).hexdigest()
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ from dataclasses import dataclass
|
||||
from typing import Mapping
|
||||
|
||||
|
||||
DEFAULT_MAX_RESPONSE_BYTES = 16 * 1024 * 1024
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HttpFetchResponse:
|
||||
status: int
|
||||
@@ -34,11 +37,29 @@ def fetch_http(
|
||||
label: str = "URL",
|
||||
method: str = "GET",
|
||||
headers: Mapping[str, str] | None = None,
|
||||
max_response_bytes: int = DEFAULT_MAX_RESPONSE_BYTES,
|
||||
) -> HttpFetchResponse:
|
||||
request = urllib.request.Request(validate_http_url(url, label=label), headers=dict(headers or {}), method=method)
|
||||
if max_response_bytes <= 0:
|
||||
raise ValueError("max_response_bytes must be positive.")
|
||||
request = urllib.request.Request(
|
||||
validate_http_url(url, label=label),
|
||||
headers=dict(headers or {}),
|
||||
method=method,
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 - URL is validated by validate_http_url. # nosec B310 # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected
|
||||
raw_length = response.headers.get("Content-Length")
|
||||
if raw_length is not None:
|
||||
try:
|
||||
declared_length = int(raw_length)
|
||||
except (TypeError, ValueError):
|
||||
declared_length = None
|
||||
if declared_length is not None and declared_length > max_response_bytes:
|
||||
raise ValueError(f"{label} response exceeds the configured size limit.")
|
||||
body = response.read(max_response_bytes + 1)
|
||||
if len(body) > max_response_bytes:
|
||||
raise ValueError(f"{label} response exceeds the configured size limit.")
|
||||
return HttpFetchResponse(
|
||||
status=int(getattr(response, "status", 0)),
|
||||
headers=dict(response.headers.items()),
|
||||
body=response.read(),
|
||||
body=body,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user