mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-26 21:53:48 +03:00
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
4 changed files with 133 additions and 61 deletions
|
@ -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 a new issue