From 811a6fbfe933aaa96a06ea519c64b2aaa922b697 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 19:10:22 -0400 Subject: [PATCH] feat(egress-proxy-addon): wildcard host matching with exact-match precedence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRD 0017 v1 deliberately punted wildcards ("Exact match in v1 — globs / wildcards are a follow-up"). Now that the supervise mirror strips `*.` to its suffix for pipelock, the addon needs to actually match wildcard hosts on its side or the route is dead weight. Addon `match_route` now does two passes: 1. Exact (case-insensitive) literal match on the hostname. 2. Wildcard suffix match: a route whose host starts with `*.` matches any request host that ends with `.`. So `*.example.com` matches `foo.example.com` and `a.b.example.com`, but NOT the apex `example.com` and not `barexample.com` (the leading `.` of the suffix is required). Exact wins — operators can layer a specific route (e.g. `api.github.com` with auth) on top of a broader wildcard (e.g. `*.github.com` bare-pass). 8 new unit tests: direct subdomain match, nested subdomain match, apex rejection, overlapping-suffix rejection, case-insensitive, exact-wins-over-wildcard (both route orders), no-match fall-through. 395 unit + integration pass. Co-Authored-By: Claude Opus 4.7 --- claude_bottle/egress_proxy_addon_core.py | 28 +++++++++-- tests/unit/test_egress_proxy_addon_core.py | 58 ++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/claude_bottle/egress_proxy_addon_core.py b/claude_bottle/egress_proxy_addon_core.py index d2bbfbf..e77cbc9 100644 --- a/claude_bottle/egress_proxy_addon_core.py +++ b/claude_bottle/egress_proxy_addon_core.py @@ -169,15 +169,33 @@ def match_route( routes: typing.Sequence[Route], request_host: str, ) -> Route | None: - """Return the first route whose `host` matches `request_host`. + """Return the route whose `host` matches `request_host`. - Exact match in v1 — globs / wildcards are a follow-up (per PRD - 0017 open questions). Hostname comparison is case-insensitive - because DNS names are case-insensitive.""" + Match precedence: + 1. Exact (case-insensitive) match on the literal hostname. + 2. Wildcard match: a route whose host starts with `*.` is a + suffix pattern. `*.example.com` matches any host that + ends with `.example.com` (so `foo.example.com` and + `a.b.example.com`, but NOT the apex `example.com` or + `barexample.com`). + + Exact match wins over wildcard so an operator can declare a + specific route on top of a broader wildcard (e.g. a + `*.github.com` bare-pass + an `api.github.com` route with + auth). DNS names are case-insensitive.""" target = request_host.lower() + # Pass 1: exact, literal hostname match. for r in routes: - if r.host.lower() == target: + host = r.host.lower() + if not host.startswith("*.") and host == target: return r + # Pass 2: wildcard suffix match (`*.foo.com` → `.foo.com`). + for r in routes: + host = r.host.lower() + if host.startswith("*."): + suffix = host[1:] # keeps the leading `.` + if target.endswith(suffix) and target != suffix[1:]: + return r return None diff --git a/tests/unit/test_egress_proxy_addon_core.py b/tests/unit/test_egress_proxy_addon_core.py index fd9de6e..ba89fce 100644 --- a/tests/unit/test_egress_proxy_addon_core.py +++ b/tests/unit/test_egress_proxy_addon_core.py @@ -148,6 +148,64 @@ class TestMatchRoute(unittest.TestCase): self.assertIsNone(match_route(self.ROUTES, "evil.api.github.com")) +class TestMatchRouteWildcards(unittest.TestCase): + """Wildcard host patterns: `*.foo.com` matches any host that + ends with `.foo.com` (subdomains, one level or more).""" + + def test_wildcard_matches_direct_subdomain(self): + routes = (Route(host="*.example.com"),) + r = match_route(routes, "foo.example.com") + self.assertIsNotNone(r) + self.assertEqual("*.example.com", r.host) + + def test_wildcard_matches_nested_subdomain(self): + routes = (Route(host="*.example.com"),) + self.assertIsNotNone(match_route(routes, "a.b.example.com")) + + def test_wildcard_does_not_match_apex(self): + routes = (Route(host="*.example.com"),) + self.assertIsNone(match_route(routes, "example.com")) + + def test_wildcard_does_not_match_overlapping_suffix(self): + # `*.example.com` shouldn't match `barexample.com` — the + # match requires `.` before the suffix. + routes = (Route(host="*.example.com"),) + self.assertIsNone(match_route(routes, "barexample.com")) + + def test_wildcard_case_insensitive(self): + routes = (Route(host="*.example.com"),) + self.assertIsNotNone(match_route(routes, "FOO.Example.COM")) + + def test_exact_match_wins_over_wildcard(self): + # A specific route declared alongside a broader wildcard + # should take precedence — operators stack a per-host + # config on top of a permissive wildcard this way. + routes = ( + Route(host="*.github.com"), + Route(host="api.github.com", auth_scheme="Bearer", + token_env="EGRESS_PROXY_TOKEN_0"), + ) + r = match_route(routes, "api.github.com") + self.assertIsNotNone(r) + self.assertEqual("api.github.com", r.host) + self.assertEqual("Bearer", r.auth_scheme) + + def test_exact_wins_regardless_of_route_order(self): + # Same as above but with wildcard declared AFTER exact — + # exact wins because pass 1 finds it before pass 2 runs. + routes = ( + Route(host="api.github.com", auth_scheme="Bearer", + token_env="EGRESS_PROXY_TOKEN_0"), + Route(host="*.github.com"), + ) + r = match_route(routes, "api.github.com") + self.assertEqual("api.github.com", r.host) + + def test_no_match_falls_through(self): + routes = (Route(host="*.example.com"),) + self.assertIsNone(match_route(routes, "elsewhere.org")) + + # --- decide --------------------------------------------------------------