fix(evidence): bind HTTP validators to final URL

This commit is contained in:
2026-07-12 03:38:11 +02:00
parent 81ff1810d1
commit d1fdf7d9f5
3 changed files with 110 additions and 21 deletions
+18 -21
View File
@@ -57,7 +57,8 @@ class HttpManifestEvidenceSource:
self._session = requests.Session()
self._session.trust_env = False
self._cache: OrderedDict[str, AcquiredDocument] = OrderedDict()
self._validators: dict[str, tuple[str | None, str | None]] = {}
# provenance -> (exact final effective URL, ETag, Last-Modified)
self._validators: dict[str, tuple[str, str | None, str | None]] = {}
self._cache_bytes = 0
def __repr__(self) -> str:
@@ -137,12 +138,14 @@ class HttpManifestEvidenceSource:
return EvidenceSourceErrorCategory.TRANSIENT
return EvidenceSourceErrorCategory.PERMANENT
def _conditional_headers(self, provenance: str) -> dict[str, str]:
def _conditional_headers(self, provenance: str, request_url: str) -> dict[str, str]:
cached = self._cache.get(self._source_id(provenance))
validators = self._validators.get(provenance)
if cached is None or validators is None:
return {}
etag, last_modified = validators
final_url, etag, last_modified = validators
if request_url != final_url:
return {}
headers = {}
if etag:
headers["If-None-Match"] = etag
@@ -153,6 +156,7 @@ class HttpManifestEvidenceSource:
def _remember(
self,
provenance: str,
final_url: str,
document: AcquiredDocument,
validators: tuple[str | None, str | None],
) -> None:
@@ -162,7 +166,7 @@ class HttpManifestEvidenceSource:
self._cache_bytes -= len(old.content)
self._cache[source_id] = document
self._cache_bytes += len(document.content)
self._validators[provenance] = validators
self._validators[provenance] = (final_url, *validators)
while self._cache and self._cache_bytes > self.max_cache_bytes:
evicted_id, evicted = self._cache.popitem(last=False)
self._cache_bytes -= len(evicted.content)
@@ -172,9 +176,9 @@ class HttpManifestEvidenceSource:
def _download(self, transport_url: str, provenance: str) -> AcquiredDocument:
current = transport_url
headers = self._conditional_headers(provenance)
try:
for redirect_count in range(self.max_redirects + 1):
headers = self._conditional_headers(provenance, current)
allowed = self._resolve_allowed(current)
response = None
try:
@@ -194,25 +198,18 @@ class HttpManifestEvidenceSource:
self._validate_url_shape(destination)
except ValueError as error:
raise self._safe_error("redirect") from error
current_origin = urlsplit(current)
destination_origin = urlsplit(destination)
if (
current_origin.scheme,
current_origin.hostname,
current_origin.port,
) != (
destination_origin.scheme,
destination_origin.hostname,
destination_origin.port,
):
headers = {}
# Resolve every hop independently. Validators are never forwarded across
# origins, where even an opaque ETag would become cross-origin state.
# The next iteration binds validators to the exact destination URL.
current = destination
continue
if response.status_code == 304:
cached = self._cache.get(self._source_id(provenance))
if cached is None:
binding = self._validators.get(provenance)
if (
cached is None
or not headers
or binding is None
or binding[0] != current
):
raise self._safe_error("conditional_response")
self._cache.move_to_end(cached.source.source_id)
return cached
@@ -273,7 +270,7 @@ class HttpManifestEvidenceSource:
media_type=media_type,
acquired_at=datetime.now(UTC),
)
self._remember(provenance, document, (etag, last_modified))
self._remember(provenance, current, document, (etag, last_modified))
return document
def discover(self):