mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-22 16:03:44 +01:00
389 lines
10 KiB
Go
389 lines
10 KiB
Go
// 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('$')
|
|
}
|
|
|
|
var wildcardHostPort bool
|
|
if typ == regexpTypeHost {
|
|
if !strings.Contains(pattern.String(), ":") {
|
|
wildcardHostPort = true
|
|
}
|
|
}
|
|
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,
|
|
wildcardHostPort: wildcardHostPort,
|
|
}, 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
|
|
// Wildcard host-port (no strict port match in hostname)
|
|
wildcardHostPort bool
|
|
}
|
|
|
|
// Match matches the regexp against the URL host or path.
|
|
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
|
|
if r.regexpType == regexpTypeHost {
|
|
host := getHost(req)
|
|
if r.wildcardHostPort {
|
|
// Don't be strict on the port match
|
|
if i := strings.Index(host, ":"); i != -1 {
|
|
host = host[:i]
|
|
}
|
|
}
|
|
return r.regexp.MatchString(host)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// 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), 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]
|
|
val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey)
|
|
if ok {
|
|
return templateKey + "=" + val
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0].
|
|
// If key was not found, empty string and false is returned.
|
|
func findFirstQueryKey(rawQuery, key string) (value string, ok bool) {
|
|
query := []byte(rawQuery)
|
|
for len(query) > 0 {
|
|
foundKey := query
|
|
if i := bytes.IndexAny(foundKey, "&;"); i >= 0 {
|
|
foundKey, query = foundKey[:i], foundKey[i+1:]
|
|
} else {
|
|
query = query[:0]
|
|
}
|
|
if len(foundKey) == 0 {
|
|
continue
|
|
}
|
|
var value []byte
|
|
if i := bytes.IndexByte(foundKey, '='); i >= 0 {
|
|
foundKey, value = foundKey[:i], foundKey[i+1:]
|
|
}
|
|
if len(foundKey) < len(key) {
|
|
// Cannot possibly be key.
|
|
continue
|
|
}
|
|
keyString, err := url.QueryUnescape(string(foundKey))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if keyString != key {
|
|
continue
|
|
}
|
|
valueString, err := url.QueryUnescape(string(value))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
return valueString, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
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)
|
|
if v.host.wildcardHostPort {
|
|
// Don't be strict on the port match
|
|
if i := strings.Index(host, ":"); i != -1 {
|
|
host = host[:i]
|
|
}
|
|
}
|
|
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]]
|
|
}
|
|
}
|