From 8ae0d6a509fd1b871457cf742369af04346933a8 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 10 May 2019 21:07:02 -0600 Subject: caddyhttp: Implement better HTTP matchers including regexp; add tests --- modules/caddyhttp/matchers.go | 199 +++++++++++++++++++++++++++++++++--------- 1 file changed, 160 insertions(+), 39 deletions(-) (limited to 'modules/caddyhttp/matchers.go') diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 7336a1b..0179dd7 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -1,8 +1,12 @@ package caddyhttp import ( + "fmt" "log" "net/http" + "net/textproto" + "net/url" + "regexp" "strings" "bitbucket.org/lightcodelabs/caddy2" @@ -10,15 +14,16 @@ import ( "go.starlark.net/starlark" ) -// TODO: Matchers should probably support regex of some sort... performance trade-offs? type ( matchHost []string matchPath []string + matchPathRE struct{ matchRegexp } matchMethod []string - matchQuery map[string][]string - matchHeader map[string][]string + matchQuery url.Values + matchHeader http.Header + matchHeaderRE map[string]*matchRegexp matchProtocol string - matchScript string + matchStarlark string ) func init() { @@ -30,6 +35,10 @@ func init() { Name: "http.matchers.path", New: func() (interface{}, error) { return matchPath{}, nil }, }) + caddy2.RegisterModule(caddy2.Module{ + Name: "http.matchers.path_regexp", + New: func() (interface{}, error) { return new(matchPathRE), nil }, + }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.method", New: func() (interface{}, error) { return matchMethod{}, nil }, @@ -42,45 +51,39 @@ func init() { Name: "http.matchers.header", New: func() (interface{}, error) { return matchHeader{}, nil }, }) + caddy2.RegisterModule(caddy2.Module{ + Name: "http.matchers.header_regexp", + New: func() (interface{}, error) { return matchHeaderRE{}, nil }, + }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.protocol", New: func() (interface{}, error) { return new(matchProtocol), nil }, }) caddy2.RegisterModule(caddy2.Module{ Name: "http.matchers.caddyscript", - New: func() (interface{}, error) { return new(matchScript), nil }, + New: func() (interface{}, error) { return new(matchStarlark), nil }, }) } -func (m matchScript) Match(r *http.Request) bool { - input := string(m) - thread := new(starlark.Thread) - env := caddyscript.MatcherEnv(r) - val, err := starlark.Eval(thread, "", input, env) - if err != nil { - log.Printf("caddyscript for matcher is invalid: attempting to evaluate expression `%v` error `%v`", input, err) - return false - } - - return val.String() == "True" -} - -func (m matchProtocol) Match(r *http.Request) bool { - switch string(m) { - case "grpc": - return r.Header.Get("content-type") == "application/grpc" - case "https": - return r.TLS != nil - case "http": - return r.TLS == nil - } - - return false -} - func (m matchHost) Match(r *http.Request) bool { +outer: for _, host := range m { - if r.Host == host { + if strings.Contains(host, "*") { + patternParts := strings.Split(host, ".") + incomingParts := strings.Split(r.Host, ".") + if len(patternParts) != len(incomingParts) { + continue + } + for i := range patternParts { + if patternParts[i] == "*" { + continue + } + if !strings.EqualFold(patternParts[i], incomingParts[i]) { + continue outer + } + } + return true + } else if strings.EqualFold(r.Host, host) { return true } } @@ -96,6 +99,11 @@ func (m matchPath) Match(r *http.Request) bool { return false } +func (m matchPathRE) Match(r *http.Request) bool { + repl := r.Context().Value(ReplacerCtxKey).(*Replacer) + return m.match(r.URL.Path, repl, "path_regexp") +} + func (m matchMethod) Match(r *http.Request) bool { for _, method := range m { if r.Method == method { @@ -118,26 +126,139 @@ func (m matchQuery) Match(r *http.Request) bool { } func (m matchHeader) Match(r *http.Request) bool { - for field, vals := range m { - fieldVals := r.Header[field] - for _, fieldVal := range fieldVals { - for _, v := range vals { - if fieldVal == v { - return true + for field, allowedFieldVals := range m { + var match bool + actualFieldVals := r.Header[textproto.CanonicalMIMEHeaderKey(field)] + fieldVals: + for _, actualFieldVal := range actualFieldVals { + for _, allowedFieldVal := range allowedFieldVals { + if actualFieldVal == allowedFieldVal { + match = true + break fieldVals } } } + if !match { + return false + } + } + return true +} + +func (m matchHeaderRE) Match(r *http.Request) bool { + for field, rm := range m { + repl := r.Context().Value(ReplacerCtxKey).(*Replacer) + match := rm.match(r.Header.Get(field), repl, "header_regexp") + if !match { + return false + } + } + return true +} + +func (m matchHeaderRE) Provision() error { + for _, rm := range m { + err := rm.Provision() + if err != nil { + return err + } + } + return nil +} + +func (m matchHeaderRE) Validate() error { + for _, rm := range m { + err := rm.Validate() + if err != nil { + return err + } + } + return nil +} + +func (m matchProtocol) Match(r *http.Request) bool { + switch string(m) { + case "grpc": + return r.Header.Get("content-type") == "application/grpc" + case "https": + return r.TLS != nil + case "http": + return r.TLS == nil } return false } +func (m matchStarlark) Match(r *http.Request) bool { + input := string(m) + thread := new(starlark.Thread) + env := caddyscript.MatcherEnv(r) + val, err := starlark.Eval(thread, "", input, env) + if err != nil { + // TODO: Can we detect this in Provision or Validate instead? + log.Printf("caddyscript for matcher is invalid: attempting to evaluate expression `%v` error `%v`", input, err) + return false + } + return val.String() == "True" +} + +// matchRegexp is just the fields common among +// matchers that can use regular expressions. +type matchRegexp struct { + Name string `json:"name"` + Pattern string `json:"pattern"` + compiled *regexp.Regexp +} + +func (mre *matchRegexp) Provision() error { + re, err := regexp.Compile(mre.Pattern) + if err != nil { + return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err) + } + mre.compiled = re + return nil +} + +func (mre *matchRegexp) Validate() error { + if mre.Name != "" && !wordRE.MatchString(mre.Name) { + return fmt.Errorf("invalid regexp name (must contain only word characters): %s", mre.Name) + } + return nil +} + +func (mre *matchRegexp) match(input string, repl *Replacer, scope string) bool { + matches := mre.compiled.FindStringSubmatch(input) + if matches == nil { + return false + } + + // save all capture groups, first by index + for i, match := range matches { + key := fmt.Sprintf("http.matchers.%s.%s.%d", scope, mre.Name, i) + repl.Map(key, match) + } + + // then by name + for i, name := range mre.compiled.SubexpNames() { + if i != 0 && name != "" { + key := fmt.Sprintf("http.matchers.%s.%s.%s", scope, mre.Name, name) + repl.Map(key, matches[i]) + } + } + + return true +} + +var wordRE = regexp.MustCompile(`\w+`) + // Interface guards var ( _ RouteMatcher = (*matchHost)(nil) _ RouteMatcher = (*matchPath)(nil) + _ RouteMatcher = (*matchPathRE)(nil) _ RouteMatcher = (*matchMethod)(nil) _ RouteMatcher = (*matchQuery)(nil) _ RouteMatcher = (*matchHeader)(nil) + _ RouteMatcher = (*matchHeaderRE)(nil) _ RouteMatcher = (*matchProtocol)(nil) - _ RouteMatcher = (*matchScript)(nil) + _ RouteMatcher = (*matchStarlark)(nil) ) -- cgit v1.2.3