|
- // Copyright 2012 The Gorilla Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
-
- package mux
-
- import (
- "bytes"
- "fmt"
- "net/http"
- "net/url"
- "regexp"
- "strconv"
- "strings"
- )
-
- type routeRegexpOptions struct {
- strictSlash bool
- useEncodedPath bool
- }
-
- type regexpType int
-
- const (
- regexpTypePath regexpType = 0
- regexpTypeHost regexpType = 1
- regexpTypePrefix regexpType = 2
- regexpTypeQuery regexpType = 3
- )
-
- // newRouteRegexp parses a route template and returns a routeRegexp,
- // used to match a host, a path or a query string.
- //
- // It will extract named variables, assemble a regexp to be matched, create
- // a "reverse" template to build URLs and compile regexps to validate variable
- // values used in URL building.
- //
- // Previously we accepted only Python-like identifiers for variable
- // names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
- // name and pattern can't be empty, and names can't contain a colon.
- func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) {
- // Check if it is well-formed.
- idxs, errBraces := braceIndices(tpl)
- if errBraces != nil {
- return nil, errBraces
- }
- // Backup the original.
- template := tpl
- // Now let's parse it.
- defaultPattern := "[^/]+"
- if typ == regexpTypeQuery {
- defaultPattern = ".*"
- } else if typ == regexpTypeHost {
- defaultPattern = "[^.]+"
- }
- // Only match strict slash if not matching
- if typ != regexpTypePath {
- options.strictSlash = false
- }
- // Set a flag for strictSlash.
- endSlash := false
- if options.strictSlash && strings.HasSuffix(tpl, "/") {
- tpl = tpl[:len(tpl)-1]
- endSlash = true
- }
- varsN := make([]string, len(idxs)/2)
- varsR := make([]*regexp.Regexp, len(idxs)/2)
- pattern := bytes.NewBufferString("")
- pattern.WriteByte('^')
- reverse := bytes.NewBufferString("")
- var end int
- var err error
- for i := 0; i < len(idxs); i += 2 {
- // Set all values we are interested in.
- raw := tpl[end:idxs[i]]
- end = idxs[i+1]
- parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2)
- name := parts[0]
- patt := defaultPattern
- if len(parts) == 2 {
- patt = parts[1]
- }
- // Name or pattern can't be empty.
- if name == "" || patt == "" {
- return nil, fmt.Errorf("mux: missing name or pattern in %q",
- tpl[idxs[i]:end])
- }
- // Build the regexp pattern.
- fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt)
-
- // Build the reverse template.
- fmt.Fprintf(reverse, "%s%%s", raw)
-
- // Append variable name and compiled pattern.
- varsN[i/2] = name
- varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
- if err != nil {
- return nil, err
- }
- }
- // Add the remaining.
- raw := tpl[end:]
- pattern.WriteString(regexp.QuoteMeta(raw))
- if options.strictSlash {
- pattern.WriteString("[/]?")
- }
- if typ == regexpTypeQuery {
- // Add the default pattern if the query value is empty
- if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" {
- pattern.WriteString(defaultPattern)
- }
- }
- if typ != regexpTypePrefix {
- pattern.WriteByte('$')
- }
- reverse.WriteString(raw)
- if endSlash {
- reverse.WriteByte('/')
- }
- // Compile full regexp.
- reg, errCompile := regexp.Compile(pattern.String())
- if errCompile != nil {
- 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!
- return &routeRegexp{
- template: template,
- regexpType: typ,
- options: options,
- regexp: reg,
- reverse: reverse.String(),
- varsN: varsN,
- varsR: varsR,
- }, nil
- }
-
- // routeRegexp stores a regexp to match a host or path and information to
- // collect and validate route variables.
- type routeRegexp struct {
- // The unmodified template.
- template string
- // The type of match
- regexpType regexpType
- // Options for matching
- options routeRegexpOptions
- // Expanded regexp.
- regexp *regexp.Regexp
- // Reverse template.
- reverse string
- // Variable names.
- varsN []string
- // Variable regexps (validators).
- varsR []*regexp.Regexp
- }
-
- // Match matches the regexp against the URL host or path.
- func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
- if r.regexpType != regexpTypeHost {
- if r.regexpType == regexpTypeQuery {
- return r.matchQueryString(req)
- }
- path := req.URL.Path
- if r.options.useEncodedPath {
- path = req.URL.EscapedPath()
- }
- return r.regexp.MatchString(path)
- }
-
- return r.regexp.MatchString(getHost(req))
- }
-
- // url builds a URL part using the given values.
- func (r *routeRegexp) url(values map[string]string) (string, error) {
- urlValues := make([]interface{}, len(r.varsN))
- for k, v := range r.varsN {
- value, ok := values[v]
- if !ok {
- return "", fmt.Errorf("mux: missing route variable %q", v)
- }
- if r.regexpType == regexpTypeQuery {
- value = url.QueryEscape(value)
- }
- urlValues[k] = value
- }
- rv := fmt.Sprintf(r.reverse, urlValues...)
- if !r.regexp.MatchString(rv) {
- // The URL is checked against the full regexp, instead of checking
- // individual variables. This is faster but to provide a good error
- // message, we check individual regexps if the URL doesn't match.
- for k, v := range r.varsN {
- if !r.varsR[k].MatchString(values[v]) {
- return "", fmt.Errorf(
- "mux: variable %q doesn't match, expected %q", values[v],
- r.varsR[k].String())
- }
- }
- }
- return rv, nil
- }
-
- // getURLQuery returns a single query parameter from a request URL.
- // For a URL with foo=bar&baz=ding, we return only the relevant key
- // value pair for the routeRegexp.
- func (r *routeRegexp) getURLQuery(req *http.Request) string {
- if r.regexpType != regexpTypeQuery {
- return ""
- }
- templateKey := strings.SplitN(r.template, "=", 2)[0]
- for key, vals := range req.URL.Query() {
- if key == templateKey && len(vals) > 0 {
- return key + "=" + vals[0]
- }
- }
- return ""
- }
-
- func (r *routeRegexp) matchQueryString(req *http.Request) bool {
- return r.regexp.MatchString(r.getURLQuery(req))
- }
-
- // braceIndices returns the first level curly brace indices from a string.
- // It returns an error in case of unbalanced braces.
- func braceIndices(s string) ([]int, error) {
- var level, idx int
- var idxs []int
- for i := 0; i < len(s); i++ {
- switch s[i] {
- case '{':
- if level++; level == 1 {
- idx = i
- }
- case '}':
- if level--; level == 0 {
- idxs = append(idxs, idx, i+1)
- } else if level < 0 {
- return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
- }
- }
- }
- if level != 0 {
- return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
- }
- return idxs, nil
- }
-
- // varGroupName builds a capturing group name for the indexed variable.
- func varGroupName(idx int) string {
- return "v" + strconv.Itoa(idx)
- }
-
- // ----------------------------------------------------------------------------
- // routeRegexpGroup
- // ----------------------------------------------------------------------------
-
- // routeRegexpGroup groups the route matchers that carry variables.
- type routeRegexpGroup struct {
- host *routeRegexp
- path *routeRegexp
- queries []*routeRegexp
- }
-
- // setMatch extracts the variables from the URL once a route matches.
- func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
- // Store host variables.
- if v.host != nil {
- host := getHost(req)
- matches := v.host.regexp.FindStringSubmatchIndex(host)
- if len(matches) > 0 {
- extractVars(host, matches, v.host.varsN, m.Vars)
- }
- }
- path := req.URL.Path
- if r.useEncodedPath {
- path = req.URL.EscapedPath()
- }
- // Store path variables.
- if v.path != nil {
- matches := v.path.regexp.FindStringSubmatchIndex(path)
- if len(matches) > 0 {
- extractVars(path, matches, v.path.varsN, m.Vars)
- // Check if we should redirect.
- if v.path.options.strictSlash {
- p1 := strings.HasSuffix(path, "/")
- p2 := strings.HasSuffix(v.path.template, "/")
- if p1 != p2 {
- u, _ := url.Parse(req.URL.String())
- if p1 {
- u.Path = u.Path[:len(u.Path)-1]
- } else {
- u.Path += "/"
- }
- m.Handler = http.RedirectHandler(u.String(), 301)
- }
- }
- }
- }
- // Store query string variables.
- for _, q := range v.queries {
- queryURL := q.getURLQuery(req)
- matches := q.regexp.FindStringSubmatchIndex(queryURL)
- if len(matches) > 0 {
- extractVars(queryURL, matches, q.varsN, m.Vars)
- }
- }
- }
-
- // getHost tries its best to return the request host.
- func getHost(r *http.Request) string {
- if r.URL.IsAbs() {
- return r.URL.Host
- }
- host := r.Host
- // Slice off any port information.
- if i := strings.Index(host, ":"); i != -1 {
- host = host[:i]
- }
- return host
-
- }
-
- func extractVars(input string, matches []int, names []string, output map[string]string) {
- for i, name := range names {
- output[name] = input[matches[2*i+2]:matches[2*i+3]]
- }
- }
|