Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions internal/broker/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,10 +585,15 @@ func MatchService(host string, targetPort int, path string, services []Service)
// matchHostPattern reports the tier and whether pattern matches host.
// "*." matches exactly one subdomain level (e.g. "*.github.com" matches
// "api.github.com" but not "a.b.github.com" or the bare "github.com").
// Hostnames are case-insensitive (RFC 4343), so both sides are lowercased.
// Only ASCII letters are folded: Unicode case mapping would turn e.g.
// U+0130 into "i" and let a different IDNA domain match a service.
func matchHostPattern(pattern, host string) (tier int, ok bool) {
if h, _, err := net.SplitHostPort(pattern); err == nil {
pattern = h
}
pattern = asciiLower(pattern)
host = asciiLower(host)
if pattern == host {
return HostTierExact, true
}
Expand All @@ -604,6 +609,17 @@ func matchHostPattern(pattern, host string) (tier int, ok bool) {
return 0, false
}

// asciiLower lowercases ASCII letters in s and leaves all other bytes alone.
func asciiLower(s string) string {
b := []byte(s)
for i, c := range b {
if 'A' <= c && c <= 'Z' {
b[i] = c + ('a' - 'A')
}
}
return string(b)
}

// AnyHostMatches reports whether any service's host pattern matches
// host, ignoring paths. Used to filter discovered hosts against the
// current service list so hosts that are already covered by a
Expand Down
45 changes: 45 additions & 0 deletions internal/broker/broker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,51 @@ func TestMatchServiceWildcard(t *testing.T) {
}
}

func TestMatchServiceHostCaseInsensitive(t *testing.T) {
services := []Service{
{Name: "stripe", Host: "api.stripe.com", Auth: Auth{Type: "bearer", Token: "STRIPE_KEY"}},
{Name: "github", Host: "*.GitHub.com", Auth: Auth{Type: "bearer", Token: "GH_TOKEN"}},
}
cases := []struct {
host string
want string
tier int
}{
{"API.Stripe.COM", "stripe", HostTierExact},
{"api.github.com", "github", HostTierWildcard},
{"Uploads.GITHUB.com", "github", HostTierWildcard},
}
for _, tc := range cases {
r, score := MatchService(tc.host, 0, "/", services)
if r == nil || r.Name != tc.want {
t.Fatalf("host %q: expected %s, got %+v", tc.host, tc.want, r)
}
if score.HostTier != tc.tier {
t.Fatalf("host %q: expected tier %d, got %d", tc.host, tc.tier, score.HostTier)
}
}
if !AnyHostMatches("API.STRIPE.COM", services) {
t.Fatal("expected AnyHostMatches to ignore host case")
}
}

func TestMatchServiceHostNonASCIINotFolded(t *testing.T) {
services := []Service{
{Name: "stripe", Host: "api.stripe.com", Auth: Auth{Type: "bearer", Token: "STRIPE_KEY"}},
{Name: "kube", Host: "*.k8s.example.com", Auth: Auth{Type: "bearer", Token: "KUBE_TOKEN"}},
}
// U+0130 and the Kelvin sign U+212A lowercase to ASCII "i" and "k"
// under Unicode rules, but name different IDNA domains.
for _, host := range []string{"api.str\u0130pe.com", "api.\u212A8s.example.com"} {
if r, _ := MatchService(host, 0, "/", services); r != nil {
t.Fatalf("host %q: expected no match, got %s", host, r.Name)
}
if AnyHostMatches(host, services) {
t.Fatalf("host %q: expected AnyHostMatches to be false", host)
}
}
}

func TestMatchServiceNoMatch(t *testing.T) {
services := []Service{
{Name: "stripe", Host: "api.stripe.com", Auth: Auth{Type: "bearer", Token: "STRIPE_KEY"}},
Expand Down