mirror of https://github.com/caddyserver/caddy.git
Allow multiple matcher sets in routes (OR'ed together)
Also export MatchRegexp in case other matcher modules find it useful. Add comments to the exported matchers.
This commit is contained in:
parent
bc00d840e8
commit
284fb3a98c
|
@ -53,13 +53,17 @@ func (app *App) Provision(ctx caddy2.Context) error {
|
|||
for i := range srv.Listen {
|
||||
srv.Listen[i] = repl.ReplaceAll(srv.Listen[i], "")
|
||||
}
|
||||
err := srv.Routes.Provision(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up server routes: %v", err)
|
||||
if srv.Routes != nil {
|
||||
err := srv.Routes.Provision(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up server routes: %v", err)
|
||||
}
|
||||
}
|
||||
err = srv.Errors.Routes.Provision(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up server error handling routes: %v", err)
|
||||
if srv.Errors != nil {
|
||||
err := srv.Errors.Routes.Provision(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up server error handling routes: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,13 +191,15 @@ func (app *App) automaticHTTPS() error {
|
|||
// find all qualifying domain names, de-duplicated
|
||||
domainSet := make(map[string]struct{})
|
||||
for _, route := range srv.Routes {
|
||||
for _, m := range route.matchers {
|
||||
if hm, ok := m.(*MatchHost); ok {
|
||||
for _, d := range *hm {
|
||||
if !certmagic.HostQualifies(d) {
|
||||
continue
|
||||
for _, matcherSet := range route.matcherSets {
|
||||
for _, m := range matcherSet {
|
||||
if hm, ok := m.(*MatchHost); ok {
|
||||
for _, d := range *hm {
|
||||
if !certmagic.HostQualifies(d) {
|
||||
continue
|
||||
}
|
||||
domainSet[d] = struct{}{}
|
||||
}
|
||||
domainSet[d] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -245,9 +251,11 @@ func (app *App) automaticHTTPS() error {
|
|||
redirTo += "{http.request.uri}"
|
||||
|
||||
redirRoutes = append(redirRoutes, ServerRoute{
|
||||
matchers: []RequestMatcher{
|
||||
MatchProtocol("http"),
|
||||
MatchHost(domains),
|
||||
matcherSets: []MatcherSet{
|
||||
{
|
||||
MatchProtocol("http"),
|
||||
MatchHost(domains),
|
||||
},
|
||||
},
|
||||
responder: Static{
|
||||
StatusCode: http.StatusTemporaryRedirect, // TODO: use permanent redirect instead
|
||||
|
|
|
@ -17,16 +17,35 @@ import (
|
|||
)
|
||||
|
||||
type (
|
||||
MatchHost []string
|
||||
MatchPath []string
|
||||
MatchPathRE struct{ matchRegexp }
|
||||
MatchMethod []string
|
||||
MatchQuery url.Values
|
||||
MatchHeader http.Header
|
||||
MatchHeaderRE map[string]*matchRegexp
|
||||
MatchProtocol string
|
||||
// MatchHost matches requests by the Host value.
|
||||
MatchHost []string
|
||||
|
||||
// MatchPath matches requests by the URI's path.
|
||||
MatchPath []string
|
||||
|
||||
// MatchPathRE matches requests by a regular expression on the URI's path.
|
||||
MatchPathRE struct{ MatchRegexp }
|
||||
|
||||
// MatchMethod matches requests by the method.
|
||||
MatchMethod []string
|
||||
|
||||
// MatchQuery matches requests by URI's query string.
|
||||
MatchQuery url.Values
|
||||
|
||||
// MatchHeader matches requests by header fields.
|
||||
MatchHeader http.Header
|
||||
|
||||
// MatchHeaderRE matches requests by a regular expression on header fields.
|
||||
MatchHeaderRE map[string]*MatchRegexp
|
||||
|
||||
// MatchProtocol matches requests by protocol.
|
||||
MatchProtocol string
|
||||
|
||||
// MatchStarlarkExpr matches requests by evaluating a Starlark expression.
|
||||
MatchStarlarkExpr string
|
||||
MatchTable string // TODO: finish implementing
|
||||
|
||||
// MatchTable matches requests by values in the table.
|
||||
MatchTable string // TODO: finish implementing
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -68,6 +87,7 @@ func init() {
|
|||
})
|
||||
}
|
||||
|
||||
// Match returns true if r matches m.
|
||||
func (m MatchHost) Match(r *http.Request) bool {
|
||||
outer:
|
||||
for _, host := range m {
|
||||
|
@ -93,6 +113,7 @@ outer:
|
|||
return false
|
||||
}
|
||||
|
||||
// Match returns true if r matches m.
|
||||
func (m MatchPath) Match(r *http.Request) bool {
|
||||
for _, matchPath := range m {
|
||||
compare := r.URL.Path
|
||||
|
@ -111,11 +132,13 @@ func (m MatchPath) Match(r *http.Request) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Match returns true if r matches m.
|
||||
func (m MatchPathRE) Match(r *http.Request) bool {
|
||||
repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
|
||||
return m.match(r.URL.Path, repl, "path_regexp")
|
||||
return m.MatchRegexp.Match(r.URL.Path, repl, "path_regexp")
|
||||
}
|
||||
|
||||
// Match returns true if r matches m.
|
||||
func (m MatchMethod) Match(r *http.Request) bool {
|
||||
for _, method := range m {
|
||||
if r.Method == method {
|
||||
|
@ -125,6 +148,7 @@ func (m MatchMethod) Match(r *http.Request) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Match returns true if r matches m.
|
||||
func (m MatchQuery) Match(r *http.Request) bool {
|
||||
for param, vals := range m {
|
||||
paramVal := r.URL.Query().Get(param)
|
||||
|
@ -137,6 +161,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Match returns true if r matches m.
|
||||
func (m MatchHeader) Match(r *http.Request) bool {
|
||||
for field, allowedFieldVals := range m {
|
||||
var match bool
|
||||
|
@ -157,10 +182,11 @@ func (m MatchHeader) Match(r *http.Request) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Match returns true if r matches m.
|
||||
func (m MatchHeaderRE) Match(r *http.Request) bool {
|
||||
for field, rm := range m {
|
||||
repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
|
||||
match := rm.match(r.Header.Get(field), repl, "header_regexp")
|
||||
match := rm.Match(r.Header.Get(field), repl, "header_regexp")
|
||||
if !match {
|
||||
return false
|
||||
}
|
||||
|
@ -168,6 +194,7 @@ func (m MatchHeaderRE) Match(r *http.Request) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Provision compiles m's regular expressions.
|
||||
func (m MatchHeaderRE) Provision() error {
|
||||
for _, rm := range m {
|
||||
err := rm.Provision()
|
||||
|
@ -178,6 +205,7 @@ func (m MatchHeaderRE) Provision() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Validate validates m's regular expressions.
|
||||
func (m MatchHeaderRE) Validate() error {
|
||||
for _, rm := range m {
|
||||
err := rm.Validate()
|
||||
|
@ -188,6 +216,7 @@ func (m MatchHeaderRE) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Match returns true if r matches m.
|
||||
func (m MatchProtocol) Match(r *http.Request) bool {
|
||||
switch string(m) {
|
||||
case "grpc":
|
||||
|
@ -200,6 +229,7 @@ func (m MatchProtocol) Match(r *http.Request) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Match returns true if r matches m.
|
||||
func (m MatchStarlarkExpr) Match(r *http.Request) bool {
|
||||
input := string(m)
|
||||
thread := new(starlark.Thread)
|
||||
|
@ -213,15 +243,16 @@ func (m MatchStarlarkExpr) Match(r *http.Request) bool {
|
|||
return val.String() == "True"
|
||||
}
|
||||
|
||||
// matchRegexp is just the fields common among
|
||||
// matchers that can use regular expressions.
|
||||
type matchRegexp struct {
|
||||
// MatchRegexp is an embeddable type for matching
|
||||
// using regular expressions.
|
||||
type MatchRegexp struct {
|
||||
Name string `json:"name"`
|
||||
Pattern string `json:"pattern"`
|
||||
compiled *regexp.Regexp
|
||||
}
|
||||
|
||||
func (mre *matchRegexp) Provision() error {
|
||||
// Provision compiles the regular expression.
|
||||
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)
|
||||
|
@ -230,14 +261,21 @@ func (mre *matchRegexp) Provision() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (mre *matchRegexp) Validate() error {
|
||||
// Validate ensures mre is set up correctly.
|
||||
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 caddy2.Replacer, scope string) bool {
|
||||
// Match returns true if input matches the compiled regular
|
||||
// expression in mre. It sets values on the replacer repl
|
||||
// associated with capture groups, using the given scope
|
||||
// (namespace). Capture groups stored to repl will take on
|
||||
// the name "http.matchers.<scope>.<mre.Name>.<N>" where
|
||||
// <N> is the name or number of the capture group.
|
||||
func (mre *MatchRegexp) Match(input string, repl caddy2.Replacer, scope string) bool {
|
||||
matches := mre.compiled.FindStringSubmatch(input)
|
||||
if matches == nil {
|
||||
return false
|
||||
|
|
|
@ -176,38 +176,38 @@ func TestPathREMatcher(t *testing.T) {
|
|||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPathRE{matchRegexp{Pattern: "/"}},
|
||||
match: MatchPathRE{MatchRegexp{Pattern: "/"}},
|
||||
input: "/",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPathRE{matchRegexp{Pattern: "/foo"}},
|
||||
match: MatchPathRE{MatchRegexp{Pattern: "/foo"}},
|
||||
input: "/foo",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPathRE{matchRegexp{Pattern: "/foo"}},
|
||||
match: MatchPathRE{MatchRegexp{Pattern: "/foo"}},
|
||||
input: "/foo/",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPathRE{matchRegexp{Pattern: "/bar"}},
|
||||
match: MatchPathRE{MatchRegexp{Pattern: "/bar"}},
|
||||
input: "/foo/",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPathRE{matchRegexp{Pattern: "^/bar"}},
|
||||
match: MatchPathRE{MatchRegexp{Pattern: "^/bar"}},
|
||||
input: "/foo/bar",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPathRE{matchRegexp{Pattern: "^/foo/(.*)/baz$", Name: "name"}},
|
||||
match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(.*)/baz$", Name: "name"}},
|
||||
input: "/foo/bar/baz",
|
||||
expect: true,
|
||||
expectRepl: map[string]string{"name.1": "bar"},
|
||||
},
|
||||
{
|
||||
match: MatchPathRE{matchRegexp{Pattern: "^/foo/(?P<myparam>.*)/baz$", Name: "name"}},
|
||||
match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(?P<myparam>.*)/baz$", Name: "name"}},
|
||||
input: "/foo/bar/baz",
|
||||
expect: true,
|
||||
expectRepl: map[string]string{"name.myparam": "bar"},
|
||||
|
@ -315,17 +315,17 @@ func TestHeaderREMatcher(t *testing.T) {
|
|||
expectRepl map[string]string
|
||||
}{
|
||||
{
|
||||
match: MatchHeaderRE{"Field": &matchRegexp{Pattern: "foo"}},
|
||||
match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "foo"}},
|
||||
input: http.Header{"Field": []string{"foo"}},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchHeaderRE{"Field": &matchRegexp{Pattern: "$foo^"}},
|
||||
match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "$foo^"}},
|
||||
input: http.Header{"Field": []string{"foobar"}},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchHeaderRE{"Field": &matchRegexp{Pattern: "^foo(.*)$", Name: "name"}},
|
||||
match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}},
|
||||
input: http.Header{"Field": []string{"foobar"}},
|
||||
expect: true,
|
||||
expectRepl: map[string]string{"name.1": "bar"},
|
||||
|
|
|
@ -12,17 +12,42 @@ import (
|
|||
// middlewares, and a responder for handling HTTP
|
||||
// requests.
|
||||
type ServerRoute struct {
|
||||
Group string `json:"group,omitempty"`
|
||||
Matchers map[string]json.RawMessage `json:"match,omitempty"`
|
||||
Apply []json.RawMessage `json:"apply,omitempty"`
|
||||
Respond json.RawMessage `json:"respond,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
MatcherSets []map[string]json.RawMessage `json:"match,omitempty"`
|
||||
Apply []json.RawMessage `json:"apply,omitempty"`
|
||||
Respond json.RawMessage `json:"respond,omitempty"`
|
||||
|
||||
Terminal bool `json:"terminal,omitempty"`
|
||||
|
||||
// decoded values
|
||||
matchers []RequestMatcher
|
||||
middleware []MiddlewareHandler
|
||||
responder Handler
|
||||
matcherSets []MatcherSet
|
||||
middleware []MiddlewareHandler
|
||||
responder Handler
|
||||
}
|
||||
|
||||
func (sr ServerRoute) anyMatcherSetMatches(r *http.Request) bool {
|
||||
for _, ms := range sr.matcherSets {
|
||||
if ms.Match(r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MatcherSet is a set of matchers which
|
||||
// must all match in order for the request
|
||||
// to be matched successfully.
|
||||
type MatcherSet []RequestMatcher
|
||||
|
||||
// Match returns true if the request matches all
|
||||
// matchers in mset.
|
||||
func (mset MatcherSet) Match(r *http.Request) bool {
|
||||
for _, m := range mset {
|
||||
if !m.Match(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RouteList is a list of server routes that can
|
||||
|
@ -33,14 +58,18 @@ type RouteList []ServerRoute
|
|||
func (routes RouteList) Provision(ctx caddy2.Context) error {
|
||||
for i, route := range routes {
|
||||
// matchers
|
||||
for modName, rawMsg := range route.Matchers {
|
||||
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
|
||||
for _, matcherSet := range route.MatcherSets {
|
||||
var matchers MatcherSet
|
||||
for modName, rawMsg := range matcherSet {
|
||||
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
|
||||
}
|
||||
matchers = append(matchers, val.(RequestMatcher))
|
||||
}
|
||||
routes[i].matchers = append(routes[i].matchers, val.(RequestMatcher))
|
||||
routes[i].matcherSets = append(routes[i].matcherSets, matchers)
|
||||
}
|
||||
routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
|
||||
routes[i].MatcherSets = nil // allow GC to deallocate - TODO: Does this help?
|
||||
|
||||
// middleware
|
||||
for j, rawMsg := range route.Apply {
|
||||
|
@ -78,13 +107,10 @@ func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Re
|
|||
var responder Handler
|
||||
groups := make(map[string]struct{})
|
||||
|
||||
routeLoop:
|
||||
for _, route := range routes {
|
||||
// see if route matches
|
||||
for _, m := range route.matchers {
|
||||
if !m.Match(req) {
|
||||
continue routeLoop
|
||||
}
|
||||
// route must match at least one of the matcher sets
|
||||
if !route.anyMatcherSetMatches(req) {
|
||||
continue
|
||||
}
|
||||
|
||||
// if route is part of a group, ensure only
|
||||
|
|
Loading…
Reference in New Issue