fix: an IPv6 domain overwrite applies only on a whole-host match, never a substring splice
overwrite_domain accepted a bare IPv6 replacement, but _apply_domain_overwrites spliced it
as a substring into any host CONTAINING the target, composing an invalid host ('internal::1'
from '.local'->'::1' on internal.local) that exploded deep in yarl at request time. An IPv6
replacement now applies ONLY when the target equals the whole request host; a substring match
is left unrewritten instead of building an invalid host. Hostname/IPv4 substring splices are
unchanged. (Registration can't distinguish whole-host from substring for a hostname target
without the request host, so the guard lives at compose time where the host is known.)
Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
+30
-3
@@ -40,6 +40,16 @@ def _is_ip_host(host) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _is_ipv6_literal(host) -> bool:
|
||||
"""whether host is a bare IPv6 literal (must be a whole-host swap, never a substring splice)"""
|
||||
if not host:
|
||||
return False
|
||||
try:
|
||||
return isinstance(ipaddress.ip_address(host), ipaddress.IPv6Address)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
class _RetryStatus(Exception):
|
||||
"""internal signal: a retryable HTTP status; carries the real Response through aretry"""
|
||||
|
||||
@@ -186,12 +196,20 @@ class ExtendedSession:
|
||||
# domain overwrites
|
||||
|
||||
def overwrite_domain(self, target, replacement):
|
||||
"""register a host-substring rewrite (target -> replacement)
|
||||
"""register a host rewrite (target -> replacement)
|
||||
|
||||
raises ValueError if the replacement is not a valid host for yarl's with_host()
|
||||
(e.g. a 'host:port' string or a pre-bracketed '[::1]') - caught here rather than
|
||||
deep in yarl at request time. a bare IPv6 literal ('::1', '2001:db8::1') is valid
|
||||
and accepted: yarl brackets it itself.
|
||||
|
||||
a hostname/IPv4 replacement rewrites any host CONTAINING target (substring splice,
|
||||
e.g. 'example.com' -> 'internal.example.com'). an IPv6 replacement can only ever be
|
||||
a WHOLE-HOST swap - splicing '::1' into the middle of a name yields an invalid host
|
||||
('internal::1') - so at request time an IPv6 replacement applies ONLY when target
|
||||
equals the whole request host; a request whose host merely CONTAINS the target (a
|
||||
substring) is left unrewritten instead of composing an invalid host and exploding
|
||||
deep in yarl. see _apply_domain_overwrites.
|
||||
"""
|
||||
try:
|
||||
URL("http://placeholder").with_host(replacement)
|
||||
@@ -204,12 +222,21 @@ class ExtendedSession:
|
||||
self.domain_overwrites[target] = replacement
|
||||
|
||||
def _apply_domain_overwrites(self, url: str) -> str:
|
||||
"""apply any host-substring rewrites to a url"""
|
||||
"""apply any host rewrites to a url
|
||||
|
||||
a hostname/IPv4 replacement splices on a substring match; an IPv6 replacement only
|
||||
applies on a whole-host (exact) match, since it can't be spliced mid-host - this pairs
|
||||
with overwrite_domain's registration guard so an IPv6 rewrite never composes an
|
||||
invalid host.
|
||||
"""
|
||||
parsed = URL(url)
|
||||
if not parsed.host:
|
||||
return url
|
||||
for target, replacement in self.domain_overwrites.items():
|
||||
if target in parsed.host:
|
||||
if _is_ipv6_literal(replacement):
|
||||
if target == parsed.host:
|
||||
return str(parsed.with_host(replacement))
|
||||
elif target in parsed.host:
|
||||
return str(parsed.with_host(parsed.host.replace(target, replacement)))
|
||||
return url
|
||||
|
||||
|
||||
Reference in New Issue
Block a user