summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/matchers.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp/matchers.go')
-rw-r--r--modules/caddyhttp/matchers.go199
1 files changed, 160 insertions, 39 deletions
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() {
@@ -31,6 +36,10 @@ func init() {
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 },
})
@@ -43,44 +52,38 @@ func init() {
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)
)