blob: 80d1f785804e996282c703c26f80c641119acd08 [file] [log] [blame]
David K. Bainbridge215e0242017-09-05 23:18:24 -07001// Copyright 2012 The Gorilla Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package mux
6
7import (
8 "bytes"
9 "fmt"
10 "net/http"
11 "net/url"
12 "regexp"
13 "strconv"
14 "strings"
15)
16
17// newRouteRegexp parses a route template and returns a routeRegexp,
18// used to match a host, a path or a query string.
19//
20// It will extract named variables, assemble a regexp to be matched, create
21// a "reverse" template to build URLs and compile regexps to validate variable
22// values used in URL building.
23//
24// Previously we accepted only Python-like identifiers for variable
25// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
26// name and pattern can't be empty, and names can't contain a colon.
27func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash, useEncodedPath bool) (*routeRegexp, error) {
28 // Check if it is well-formed.
29 idxs, errBraces := braceIndices(tpl)
30 if errBraces != nil {
31 return nil, errBraces
32 }
33 // Backup the original.
34 template := tpl
35 // Now let's parse it.
36 defaultPattern := "[^/]+"
37 if matchQuery {
38 defaultPattern = ".*"
39 } else if matchHost {
40 defaultPattern = "[^.]+"
41 matchPrefix = false
42 }
43 // Only match strict slash if not matching
44 if matchPrefix || matchHost || matchQuery {
45 strictSlash = false
46 }
47 // Set a flag for strictSlash.
48 endSlash := false
49 if strictSlash && strings.HasSuffix(tpl, "/") {
50 tpl = tpl[:len(tpl)-1]
51 endSlash = true
52 }
53 varsN := make([]string, len(idxs)/2)
54 varsR := make([]*regexp.Regexp, len(idxs)/2)
55 pattern := bytes.NewBufferString("")
56 pattern.WriteByte('^')
57 reverse := bytes.NewBufferString("")
58 var end int
59 var err error
60 for i := 0; i < len(idxs); i += 2 {
61 // Set all values we are interested in.
62 raw := tpl[end:idxs[i]]
63 end = idxs[i+1]
64 parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2)
65 name := parts[0]
66 patt := defaultPattern
67 if len(parts) == 2 {
68 patt = parts[1]
69 }
70 // Name or pattern can't be empty.
71 if name == "" || patt == "" {
72 return nil, fmt.Errorf("mux: missing name or pattern in %q",
73 tpl[idxs[i]:end])
74 }
75 // Build the regexp pattern.
76 fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt)
77
78 // Build the reverse template.
79 fmt.Fprintf(reverse, "%s%%s", raw)
80
81 // Append variable name and compiled pattern.
82 varsN[i/2] = name
83 varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
84 if err != nil {
85 return nil, err
86 }
87 }
88 // Add the remaining.
89 raw := tpl[end:]
90 pattern.WriteString(regexp.QuoteMeta(raw))
91 if strictSlash {
92 pattern.WriteString("[/]?")
93 }
94 if matchQuery {
95 // Add the default pattern if the query value is empty
96 if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" {
97 pattern.WriteString(defaultPattern)
98 }
99 }
100 if !matchPrefix {
101 pattern.WriteByte('$')
102 }
103 reverse.WriteString(raw)
104 if endSlash {
105 reverse.WriteByte('/')
106 }
107 // Compile full regexp.
108 reg, errCompile := regexp.Compile(pattern.String())
109 if errCompile != nil {
110 return nil, errCompile
111 }
112
113 // Check for capturing groups which used to work in older versions
114 if reg.NumSubexp() != len(idxs)/2 {
115 panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) +
116 "Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)")
117 }
118
119 // Done!
120 return &routeRegexp{
121 template: template,
122 matchHost: matchHost,
123 matchQuery: matchQuery,
124 strictSlash: strictSlash,
125 useEncodedPath: useEncodedPath,
126 regexp: reg,
127 reverse: reverse.String(),
128 varsN: varsN,
129 varsR: varsR,
130 }, nil
131}
132
133// routeRegexp stores a regexp to match a host or path and information to
134// collect and validate route variables.
135type routeRegexp struct {
136 // The unmodified template.
137 template string
138 // True for host match, false for path or query string match.
139 matchHost bool
140 // True for query string match, false for path and host match.
141 matchQuery bool
142 // The strictSlash value defined on the route, but disabled if PathPrefix was used.
143 strictSlash bool
144 // Determines whether to use encoded path from getPath function or unencoded
145 // req.URL.Path for path matching
146 useEncodedPath bool
147 // Expanded regexp.
148 regexp *regexp.Regexp
149 // Reverse template.
150 reverse string
151 // Variable names.
152 varsN []string
153 // Variable regexps (validators).
154 varsR []*regexp.Regexp
155}
156
157// Match matches the regexp against the URL host or path.
158func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
159 if !r.matchHost {
160 if r.matchQuery {
161 return r.matchQueryString(req)
162 }
163 path := req.URL.Path
164 if r.useEncodedPath {
165 path = getPath(req)
166 }
167 return r.regexp.MatchString(path)
168 }
169
170 return r.regexp.MatchString(getHost(req))
171}
172
173// url builds a URL part using the given values.
174func (r *routeRegexp) url(values map[string]string) (string, error) {
175 urlValues := make([]interface{}, len(r.varsN))
176 for k, v := range r.varsN {
177 value, ok := values[v]
178 if !ok {
179 return "", fmt.Errorf("mux: missing route variable %q", v)
180 }
181 if r.matchQuery {
182 value = url.QueryEscape(value)
183 }
184 urlValues[k] = value
185 }
186 rv := fmt.Sprintf(r.reverse, urlValues...)
187 if !r.regexp.MatchString(rv) {
188 // The URL is checked against the full regexp, instead of checking
189 // individual variables. This is faster but to provide a good error
190 // message, we check individual regexps if the URL doesn't match.
191 for k, v := range r.varsN {
192 if !r.varsR[k].MatchString(values[v]) {
193 return "", fmt.Errorf(
194 "mux: variable %q doesn't match, expected %q", values[v],
195 r.varsR[k].String())
196 }
197 }
198 }
199 return rv, nil
200}
201
202// getURLQuery returns a single query parameter from a request URL.
203// For a URL with foo=bar&baz=ding, we return only the relevant key
204// value pair for the routeRegexp.
205func (r *routeRegexp) getURLQuery(req *http.Request) string {
206 if !r.matchQuery {
207 return ""
208 }
209 templateKey := strings.SplitN(r.template, "=", 2)[0]
210 for key, vals := range req.URL.Query() {
211 if key == templateKey && len(vals) > 0 {
212 return key + "=" + vals[0]
213 }
214 }
215 return ""
216}
217
218func (r *routeRegexp) matchQueryString(req *http.Request) bool {
219 return r.regexp.MatchString(r.getURLQuery(req))
220}
221
222// braceIndices returns the first level curly brace indices from a string.
223// It returns an error in case of unbalanced braces.
224func braceIndices(s string) ([]int, error) {
225 var level, idx int
226 var idxs []int
227 for i := 0; i < len(s); i++ {
228 switch s[i] {
229 case '{':
230 if level++; level == 1 {
231 idx = i
232 }
233 case '}':
234 if level--; level == 0 {
235 idxs = append(idxs, idx, i+1)
236 } else if level < 0 {
237 return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
238 }
239 }
240 }
241 if level != 0 {
242 return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
243 }
244 return idxs, nil
245}
246
247// varGroupName builds a capturing group name for the indexed variable.
248func varGroupName(idx int) string {
249 return "v" + strconv.Itoa(idx)
250}
251
252// ----------------------------------------------------------------------------
253// routeRegexpGroup
254// ----------------------------------------------------------------------------
255
256// routeRegexpGroup groups the route matchers that carry variables.
257type routeRegexpGroup struct {
258 host *routeRegexp
259 path *routeRegexp
260 queries []*routeRegexp
261}
262
263// setMatch extracts the variables from the URL once a route matches.
264func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
265 // Store host variables.
266 if v.host != nil {
267 host := getHost(req)
268 matches := v.host.regexp.FindStringSubmatchIndex(host)
269 if len(matches) > 0 {
270 extractVars(host, matches, v.host.varsN, m.Vars)
271 }
272 }
273 path := req.URL.Path
274 if r.useEncodedPath {
275 path = getPath(req)
276 }
277 // Store path variables.
278 if v.path != nil {
279 matches := v.path.regexp.FindStringSubmatchIndex(path)
280 if len(matches) > 0 {
281 extractVars(path, matches, v.path.varsN, m.Vars)
282 // Check if we should redirect.
283 if v.path.strictSlash {
284 p1 := strings.HasSuffix(path, "/")
285 p2 := strings.HasSuffix(v.path.template, "/")
286 if p1 != p2 {
287 u, _ := url.Parse(req.URL.String())
288 if p1 {
289 u.Path = u.Path[:len(u.Path)-1]
290 } else {
291 u.Path += "/"
292 }
293 m.Handler = http.RedirectHandler(u.String(), 301)
294 }
295 }
296 }
297 }
298 // Store query string variables.
299 for _, q := range v.queries {
300 queryURL := q.getURLQuery(req)
301 matches := q.regexp.FindStringSubmatchIndex(queryURL)
302 if len(matches) > 0 {
303 extractVars(queryURL, matches, q.varsN, m.Vars)
304 }
305 }
306}
307
308// getHost tries its best to return the request host.
309func getHost(r *http.Request) string {
310 if r.URL.IsAbs() {
311 return r.URL.Host
312 }
313 host := r.Host
314 // Slice off any port information.
315 if i := strings.Index(host, ":"); i != -1 {
316 host = host[:i]
317 }
318 return host
319
320}
321
322func extractVars(input string, matches []int, names []string, output map[string]string) {
323 for i, name := range names {
324 output[name] = input[matches[2*i+2]:matches[2*i+3]]
325 }
326}