// 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(), http.StatusMovedPermanently) } } } } // 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. // According to section 14.23 of RFC 2616 the Host header // can include the port number if the default value of 80 is not used. func getHost(r *http.Request) string { if r.URL.IsAbs() { return r.URL.Host } return r.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]] } }