This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import ipaddress
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
@@ -53,6 +54,38 @@ class FetchTimeoutError(FetchError):
|
||||
pass
|
||||
|
||||
|
||||
def _connected_peer_ip(
|
||||
response: httpx.Response,
|
||||
) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
|
||||
"""Return the actual remote peer when httpcore exposes its network stream."""
|
||||
stream = response.extensions.get("network_stream")
|
||||
if stream is None or not hasattr(stream, "get_extra_info"):
|
||||
return None
|
||||
try:
|
||||
server_address = stream.get_extra_info("server_addr")
|
||||
raw_address = server_address[0] if server_address else None
|
||||
return ipaddress.ip_address(raw_address) if raw_address else None
|
||||
except (IndexError, TypeError, ValueError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def _validate_connected_peer(
|
||||
response: httpx.Response,
|
||||
*,
|
||||
before: ValidatedUrl,
|
||||
after: ValidatedUrl,
|
||||
) -> None:
|
||||
peer_ip = _connected_peer_ip(response)
|
||||
if peer_ip is not None:
|
||||
if not peer_ip.is_global:
|
||||
raise FetchError(f"Niet-publiek verbonden IP-adres geblokkeerd: {peer_ip}")
|
||||
return
|
||||
# Mock/custom transports do not always expose the peer socket. Keep the conservative
|
||||
# DNS-overlap fallback for those transports.
|
||||
if not set(before.addresses).intersection(after.addresses):
|
||||
raise FetchError("DNS-rebindcontrole faalde bij het benaderen van bron.")
|
||||
|
||||
|
||||
def parse_retry_after(value: str | None) -> int | None:
|
||||
if not value:
|
||||
return None
|
||||
@@ -127,26 +160,22 @@ def fetch_url(
|
||||
verify=trusted_tls_context(),
|
||||
)
|
||||
current_url = url
|
||||
previous_validation: ValidatedUrl | None = None
|
||||
try:
|
||||
for _ in range(settings.FETCHER_MAX_REDIRECTS + 1):
|
||||
validation = validate_public_url(
|
||||
current_url,
|
||||
allow_nonstandard_ports=settings.FETCHER_ALLOW_NONSTANDARD_PORTS,
|
||||
)
|
||||
if (
|
||||
previous_validation is not None
|
||||
and validation.hostname == previous_validation.hostname
|
||||
and not set(validation.addresses).intersection(previous_validation.addresses)
|
||||
):
|
||||
raise FetchError("DNS-rebindcontrole faalde bij redirect naar dezelfde host.")
|
||||
response = http_client.get(current_url, headers=headers)
|
||||
post_validation = validate_public_url(
|
||||
str(response.url) if response.url else current_url,
|
||||
allow_nonstandard_ports=settings.FETCHER_ALLOW_NONSTANDARD_PORTS,
|
||||
)
|
||||
if not set(validation.addresses).intersection(set(post_validation.addresses)):
|
||||
raise FetchError("DNS-rebindcontrole faalde bij het benaderen van bron.")
|
||||
_validate_connected_peer(
|
||||
response,
|
||||
before=validation,
|
||||
after=post_validation,
|
||||
)
|
||||
if response.status_code in {301, 302, 303, 307, 308}:
|
||||
location = response.headers.get("location")
|
||||
if not location:
|
||||
@@ -155,7 +184,6 @@ def fetch_url(
|
||||
next_decision = assess_url(current_url, source=source)
|
||||
if not next_decision.allowed:
|
||||
raise PolicyBlockedError(next_decision.reason)
|
||||
previous_validation = validation
|
||||
continue
|
||||
if response.status_code == 304:
|
||||
return FetchedDocument(url, current_url, 304, dict(response.headers), b"")
|
||||
|
||||
Reference in New Issue
Block a user