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
24 changes: 18 additions & 6 deletions regexp.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,7 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
host := getHost(req)
if r.wildcardHostPort {
// Don't be strict on the port match
if i := strings.Index(host, ":"); i != -1 {
host = host[:i]
}
host = stripHostPort(host)
}
return r.regexp.MatchString(host)
}
Expand Down Expand Up @@ -343,9 +341,7 @@ func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
host := getHost(req)
if v.host.wildcardHostPort {
// Don't be strict on the port match
if i := strings.Index(host, ":"); i != -1 {
host = host[:i]
}
host = stripHostPort(host)
}
matches := v.host.regexp.FindStringSubmatchIndex(host)
if len(matches) > 0 {
Expand Down Expand Up @@ -403,6 +399,22 @@ func getHost(r *http.Request) string {
return r.Host
}

// stripHostPort returns host without its optional ":port" suffix.
// An IPv6 literal is enclosed in square brackets (RFC 3986, 3.2.2), so its port
// is separated by the colon following the closing bracket, not by the first
// colon of the host.
func stripHostPort(host string) string {
if strings.HasPrefix(host, "[") {
if i := strings.LastIndex(host, "]"); i != -1 {
return host[:i+1]
}
}
if i := strings.Index(host, ":"); i != -1 {
return host[:i]
}
return host
}

func extractVars(input string, matches []int, names []string, output map[string]string) map[string]string {
for i, name := range names {
if output == nil {
Expand Down
62 changes: 62 additions & 0 deletions regexp_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package mux

import (
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
Expand Down Expand Up @@ -115,3 +117,63 @@ func Benchmark_findQueryKeyGoLib(b *testing.B) {
})
}
}

// TestHostVarWithIPv6 checks that the optional port is stripped from a host
// before it is matched against a host template, and that the host variable then
// captures the whole host. IPv6 literals are bracketed (RFC 3986, 3.2.2), so the
// port is separated by the colon following the closing bracket, not by the first
// colon of the host.
func TestHostVarWithIPv6(t *testing.T) {
tests := []struct {
hostHeader string
wantVar string
}{
{"[::1]", "[::1]"},
{"[::1]:8080", "[::1]"},
{"[2001:db8::1]:80", "[2001:db8::1]"},
// Not bracketed: the first colon is the port separator. The default
// host pattern is "[^.]+", so the host must not contain a dot.
{"example:8080", "example"},
{"example", "example"},
}

for _, tc := range tests {
t.Run(tc.hostHeader, func(t *testing.T) {
var matched bool
var got string
r := NewRouter()
r.NewRoute().Host("{host}").HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
matched = true
got = Vars(req)["host"]
})

req := httptest.NewRequest("GET", "http://"+tc.hostHeader+"/", nil)
r.ServeHTTP(httptest.NewRecorder(), req)

if !matched {
t.Fatalf("Host(%q) did not match the request", "{host}")
}
if got != tc.wantVar {
t.Errorf("Vars(req)[%q] = %q, want %q", "host", got, tc.wantVar)
}
})
}
}

// TestHostMatchIPv6Literal checks that a host template containing an IPv6
// literal is matched against the full literal, and not against the "[" that is
// left over when the host is split on its first colon.
func TestHostMatchIPv6Literal(t *testing.T) {
var matched bool
r := NewRouter()
r.NewRoute().Host("[{host}]").HandlerFunc(func(http.ResponseWriter, *http.Request) {
matched = true
})

req := httptest.NewRequest("GET", "http://[::1]/", nil)
r.ServeHTTP(httptest.NewRecorder(), req)

if !matched {
t.Errorf(`Host("[{host}]") did not match the request host "[::1]"`)
}
}