diff --git a/src/aioweb/session.py b/src/aioweb/session.py index 8c3c189..abea816 100644 --- a/src/aioweb/session.py +++ b/src/aioweb/session.py @@ -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