Regex capturing group.

This commit is contained in:
Ludovic Fernandez 2017-10-23 10:20:02 +02:00 committed by Traefiker
parent afbad56012
commit 81f7aa9df2
6 changed files with 152 additions and 42 deletions

2
glide.lock generated
View file

@ -89,7 +89,7 @@ imports:
- name: github.com/containous/flaeg - name: github.com/containous/flaeg
version: b5d2dc5878df07c2d74413348186982e7b865871 version: b5d2dc5878df07c2d74413348186982e7b865871
- name: github.com/containous/mux - name: github.com/containous/mux
version: af6ea922f7683d9706834157e6b0610e22ccb2db version: 06ccd3e75091eb659b1d720cda0e16bc7057954c
- name: github.com/containous/staert - name: github.com/containous/staert
version: 1e26a71803e428fd933f5f9c8e50a26878f53147 version: 1e26a71803e428fd933f5f9c8e50a26878f53147
- name: github.com/coreos/etcd - name: github.com/coreos/etcd

View file

@ -136,6 +136,57 @@ func TestPriorites(t *testing.T) {
assert.NotEqual(t, foobarMatcher.Handler, fooHandler, "Error matching priority") assert.NotEqual(t, foobarMatcher.Handler, fooHandler, "Error matching priority")
} }
func TestHostRegexp(t *testing.T) {
testCases := []struct {
desc string
hostExp string
urls map[string]bool
}{
{
desc: "capturing group",
hostExp: "{subdomain:(foo\\.)?bar\\.com}",
urls: map[string]bool{
"http://foo.bar.com": true,
"http://bar.com": true,
"http://fooubar.com": false,
"http://barucom": false,
"http://barcom": false,
},
},
{
desc: "non capturing group",
hostExp: "{subdomain:(?:foo\\.)?bar\\.com}",
urls: map[string]bool{
"http://foo.bar.com": true,
"http://bar.com": true,
"http://fooubar.com": false,
"http://barucom": false,
"http://barcom": false,
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rls := &Rules{
route: &serverRoute{
route: &mux.Route{},
},
}
rt := rls.hostRegexp(test.hostExp)
for testURL, match := range test.urls {
req := testhelpers.MustNewRequest(http.MethodGet, testURL, nil)
assert.Equal(t, match, rt.Match(req, &mux.RouteMatch{}))
}
})
}
}
type fakeHandler struct { type fakeHandler struct {
name string name string
} }

View file

@ -57,11 +57,6 @@ calling mux.Vars():
vars := mux.Vars(request) vars := mux.Vars(request)
category := vars["category"] category := vars["category"]
Note that if any capturing groups are present, mux will panic() during parsing. To prevent
this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to
"/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably
when capturing groups were present.
And this is all you need to know about the basic usage. More advanced options And this is all you need to know about the basic usage. More advanced options
are explained below. are explained below.

View file

@ -11,7 +11,10 @@ import (
"path" "path"
"regexp" "regexp"
"sort" "sort"
"strings" )
var (
ErrMethodMismatch = errors.New("method is not allowed")
) )
// NewRouter returns a new router instance. // NewRouter returns a new router instance.
@ -40,6 +43,10 @@ func NewRouter() *Router {
type Router struct { type Router struct {
// Configurable Handler to be used when no route matches. // Configurable Handler to be used when no route matches.
NotFoundHandler http.Handler NotFoundHandler http.Handler
// Configurable Handler to be used when the request method does not match the route.
MethodNotAllowedHandler http.Handler
// Parent route, if this is a subrouter. // Parent route, if this is a subrouter.
parent parentRoute parent parentRoute
// Routes to be matched, in order. // Routes to be matched, in order.
@ -66,6 +73,11 @@ func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
} }
} }
if match.MatchErr == ErrMethodMismatch && r.MethodNotAllowedHandler != nil {
match.Handler = r.MethodNotAllowedHandler
return true
}
// Closest match for a router (includes sub-routers) // Closest match for a router (includes sub-routers)
if r.NotFoundHandler != nil { if r.NotFoundHandler != nil {
match.Handler = r.NotFoundHandler match.Handler = r.NotFoundHandler
@ -82,7 +94,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if !r.skipClean { if !r.skipClean {
path := req.URL.Path path := req.URL.Path
if r.useEncodedPath { if r.useEncodedPath {
path = getPath(req) path = req.URL.EscapedPath()
} }
// Clean path to canonical form and redirect. // Clean path to canonical form and redirect.
if p := cleanPath(path); p != path { if p := cleanPath(path); p != path {
@ -106,9 +118,15 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
req = setVars(req, match.Vars) req = setVars(req, match.Vars)
req = setCurrentRoute(req, match.Route) req = setCurrentRoute(req, match.Route)
} }
if handler == nil && match.MatchErr == ErrMethodMismatch {
handler = methodNotAllowedHandler()
}
if handler == nil { if handler == nil {
handler = http.NotFoundHandler() handler = http.NotFoundHandler()
} }
if !r.KeepContext { if !r.KeepContext {
defer contextClear(req) defer contextClear(req)
} }
@ -356,6 +374,11 @@ type RouteMatch struct {
Route *Route Route *Route
Handler http.Handler Handler http.Handler
Vars map[string]string Vars map[string]string
// MatchErr is set to appropriate matching error
// It is set to ErrMethodMismatch if there is a mismatch in
// the request method and route method
MatchErr error
} }
type contextKey int type contextKey int
@ -397,28 +420,6 @@ func setCurrentRoute(r *http.Request, val interface{}) *http.Request {
// Helpers // Helpers
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// getPath returns the escaped path if possible; doing what URL.EscapedPath()
// which was added in go1.5 does
func getPath(req *http.Request) string {
if req.RequestURI != "" {
// Extract the path from RequestURI (which is escaped unlike URL.Path)
// as detailed here as detailed in https://golang.org/pkg/net/url/#URL
// for < 1.5 server side workaround
// http://localhost/path/here?v=1 -> /path/here
path := req.RequestURI
path = strings.TrimPrefix(path, req.URL.Scheme+`://`)
path = strings.TrimPrefix(path, req.URL.Host)
if i := strings.LastIndex(path, "?"); i > -1 {
path = path[:i]
}
if i := strings.LastIndex(path, "#"); i > -1 {
path = path[:i]
}
return path
}
return req.URL.Path
}
// cleanPath returns the canonical path for p, eliminating . and .. elements. // cleanPath returns the canonical path for p, eliminating . and .. elements.
// Borrowed from the net/http package. // Borrowed from the net/http package.
func cleanPath(p string) string { func cleanPath(p string) string {
@ -557,3 +558,12 @@ func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]s
} }
return true return true
} }
// methodNotAllowed replies to the request with an HTTP status code 405.
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
}
// methodNotAllowedHandler returns a simple request handler
// that replies to each request with a status code 405.
func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) }

View file

@ -109,13 +109,6 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash,
if errCompile != nil { if errCompile != nil {
return nil, errCompile return nil, errCompile
} }
// Check for capturing groups which used to work in older versions
if reg.NumSubexp() != len(idxs)/2 {
panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) +
"Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)")
}
// Done! // Done!
return &routeRegexp{ return &routeRegexp{
template: template, template: template,
@ -141,7 +134,7 @@ type routeRegexp struct {
matchQuery bool matchQuery bool
// The strictSlash value defined on the route, but disabled if PathPrefix was used. // The strictSlash value defined on the route, but disabled if PathPrefix was used.
strictSlash bool strictSlash bool
// Determines whether to use encoded path from getPath function or unencoded // Determines whether to use encoded req.URL.EnscapedPath() or unencoded
// req.URL.Path for path matching // req.URL.Path for path matching
useEncodedPath bool useEncodedPath bool
// Expanded regexp. // Expanded regexp.
@ -162,7 +155,7 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
} }
path := req.URL.Path path := req.URL.Path
if r.useEncodedPath { if r.useEncodedPath {
path = getPath(req) path = req.URL.EscapedPath()
} }
return r.regexp.MatchString(path) return r.regexp.MatchString(path)
} }
@ -272,7 +265,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
} }
path := req.URL.Path path := req.URL.Path
if r.useEncodedPath { if r.useEncodedPath {
path = getPath(req) path = req.URL.EscapedPath()
} }
// Store path variables. // Store path variables.
if v.path != nil { if v.path != nil {
@ -320,7 +313,14 @@ func getHost(r *http.Request) string {
} }
func extractVars(input string, matches []int, names []string, output map[string]string) { func extractVars(input string, matches []int, names []string, output map[string]string) {
for i, name := range names { matchesCount := 0
output[name] = input[matches[2*i+2]:matches[2*i+3]] prevEnd := -1
for i := 2; i < len(matches) && matchesCount < len(names); i += 2 {
if prevEnd < matches[i+1] {
value := input[matches[i]:matches[i+1]]
output[names[matchesCount]] = value
prevEnd = matches[i+1]
matchesCount++
}
} }
} }

View file

@ -54,12 +54,27 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
if r.buildOnly || r.err != nil { if r.buildOnly || r.err != nil {
return false return false
} }
var matchErr error
// Match everything. // Match everything.
for _, m := range r.matchers { for _, m := range r.matchers {
if matched := m.Match(req, match); !matched { if matched := m.Match(req, match); !matched {
if _, ok := m.(methodMatcher); ok {
matchErr = ErrMethodMismatch
continue
}
matchErr = nil
return false return false
} }
} }
if matchErr != nil {
match.MatchErr = matchErr
return false
}
match.MatchErr = nil
// Yay, we have a match. Let's collect some info about it. // Yay, we have a match. Let's collect some info about it.
if match.Route == nil { if match.Route == nil {
match.Route = r match.Route = r
@ -70,6 +85,7 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
if match.Vars == nil { if match.Vars == nil {
match.Vars = make(map[string]string) match.Vars = make(map[string]string)
} }
// Set variables. // Set variables.
if r.regexp != nil { if r.regexp != nil {
r.regexp.setMatch(req, match, r) r.regexp.setMatch(req, match, r)
@ -607,6 +623,44 @@ func (r *Route) GetPathRegexp() (string, error) {
return r.regexp.path.regexp.String(), nil return r.regexp.path.regexp.String(), nil
} }
// GetQueriesRegexp returns the expanded regular expressions used to match the
// route queries.
// This is useful for building simple REST API documentation and for instrumentation
// against third-party services.
// An empty list will be returned if the route does not have queries.
func (r *Route) GetQueriesRegexp() ([]string, error) {
if r.err != nil {
return nil, r.err
}
if r.regexp == nil || r.regexp.queries == nil {
return nil, errors.New("mux: route doesn't have queries")
}
var queries []string
for _, query := range r.regexp.queries {
queries = append(queries, query.regexp.String())
}
return queries, nil
}
// GetQueriesTemplates returns the templates used to build the
// query matching.
// This is useful for building simple REST API documentation and for instrumentation
// against third-party services.
// An empty list will be returned if the route does not define queries.
func (r *Route) GetQueriesTemplates() ([]string, error) {
if r.err != nil {
return nil, r.err
}
if r.regexp == nil || r.regexp.queries == nil {
return nil, errors.New("mux: route doesn't have queries")
}
var queries []string
for _, query := range r.regexp.queries {
queries = append(queries, query.template)
}
return queries, nil
}
// GetMethods returns the methods the route matches against // GetMethods returns the methods the route matches against
// This is useful for building simple REST API documentation and for instrumentation // This is useful for building simple REST API documentation and for instrumentation
// against third-party services. // against third-party services.