diff options
-rw-r--r-- | caddyconfig/httpcaddyfile/builtins.go | 33 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/directives.go | 1 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/httptype.go | 138 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/parser_test.go | 4 | ||||
-rw-r--r-- | cmd/main.go | 7 | ||||
-rw-r--r-- | modules/caddyhttp/matchers.go | 39 | ||||
-rw-r--r-- | modules/caddyhttp/matchers_test.go | 38 |
7 files changed, 205 insertions, 55 deletions
diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index b523d95..ebb03cc 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -33,6 +33,7 @@ func init() { RegisterDirective("tls", parseTLS) RegisterHandlerDirective("redir", parseRedir) RegisterHandlerDirective("respond", parseRespond) + RegisterHandlerDirective("route", parseRoute) } func parseBind(h Helper) ([]ConfigValue, error) { @@ -297,3 +298,35 @@ func parseRespond(h Helper) (caddyhttp.MiddlewareHandler, error) { } return sr, nil } + +func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) { + sr := new(caddyhttp.Subroute) + + for h.Next() { + for nesting := h.Nesting(); h.NextBlock(nesting); { + dir := h.Val() + + dirFunc, ok := registeredDirectives[dir] + if !ok { + return nil, h.Errf("unrecognized directive: %s", dir) + } + + subHelper := h + subHelper.Dispenser = h.NewFromNextTokens() + + results, err := dirFunc(subHelper) + if err != nil { + return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err) + } + for _, result := range results { + handler, ok := result.Value.(caddyhttp.Route) + if !ok { + return nil, h.Errf("%s directive returned something other than an HTTP route: %#v (only handler directives can be used in routes)", dir, result.Value) + } + sr.Routes = append(sr.Routes, handler) + } + } + } + + return sr, nil +} diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index 8f30db2..19ecb26 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -36,6 +36,7 @@ var defaultDirectiveOrder = []string{ "request_header", "encode", "templates", + "route", "redir", "respond", "reverse_proxy", diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index d8fde46..64b93b0 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -114,37 +114,45 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, } // extract matcher definitions - d := sb.block.DispenseDirective("matcher") - matcherDefs, err := parseMatcherDefinitions(d) - if err != nil { - return nil, warnings, err + matcherDefs := make(map[string]caddy.ModuleMap) + for _, segment := range sb.block.Segments { + if dir := segment.Directive(); strings.HasPrefix(dir, matcherPrefix) { + d := sb.block.DispenseDirective(dir) + err := parseMatcherDefinitions(d, matcherDefs) + if err != nil { + return nil, warnings, err + } + } } for _, segment := range sb.block.Segments { dir := segment.Directive() - if dir == "matcher" { - // TODO: This is a special case because we pre-processed it; handle this better + + if strings.HasPrefix(dir, matcherPrefix) { + // matcher definitions were pre-processed continue } - if dirFunc, ok := registeredDirectives[dir]; ok { - results, err := dirFunc(Helper{ - Dispenser: caddyfile.NewDispenser(segment), - options: options, - warnings: &warnings, - matcherDefs: matcherDefs, - parentBlock: sb.block, - }) - if err != nil { - return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err) - } - for _, result := range results { - result.directive = dir - sb.pile[result.Class] = append(sb.pile[result.Class], result) - } - } else { + + dirFunc, ok := registeredDirectives[dir] + if !ok { tkn := segment[0] return nil, warnings, fmt.Errorf("%s:%d: unrecognized directive: %s", tkn.File, tkn.Line, dir) } + + results, err := dirFunc(Helper{ + Dispenser: caddyfile.NewDispenser(segment), + options: options, + warnings: &warnings, + matcherDefs: matcherDefs, + parentBlock: sb.block, + }) + if err != nil { + return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err) + } + for _, result := range results { + result.directive = dir + sb.pile[result.Class] = append(sb.pile[result.Class], result) + } } } @@ -372,10 +380,63 @@ func (st *ServerType) serversFromPairings( } sort.SliceStable(dirRoutes, func(i, j int) bool { iDir, jDir := dirRoutes[i].directive, dirRoutes[j].directive + if iDir == jDir { + // TODO: we really need to refactor this into a separate function or method... + // sub-sort by path matcher length, if there's only one + iRoute := dirRoutes[i].Value.(caddyhttp.Route) + jRoute := dirRoutes[j].Value.(caddyhttp.Route) + if len(iRoute.MatcherSetsRaw) == 1 && len(jRoute.MatcherSetsRaw) == 1 { + // for slightly better efficiency, only decode the path matchers once, + // then just store them arbitrarily in the decoded MatcherSets field, + // ours should be the only thing in there + var iPM, jPM caddyhttp.MatchPath + if len(iRoute.MatcherSets) == 1 { + iPM = iRoute.MatcherSets[0][0].(caddyhttp.MatchPath) + } + if len(jRoute.MatcherSets) == 1 { + jPM = jRoute.MatcherSets[0][0].(caddyhttp.MatchPath) + } + // if it's our first time seeing this route's path matcher, decode it + if iPM == nil { + var pathMatcher caddyhttp.MatchPath + _ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &pathMatcher) + iRoute.MatcherSets = caddyhttp.MatcherSets{{pathMatcher}} + iPM = pathMatcher + } + if jPM == nil { + var pathMatcher caddyhttp.MatchPath + _ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &pathMatcher) + jRoute.MatcherSets = caddyhttp.MatcherSets{{pathMatcher}} + jPM = pathMatcher + } + // finally, if there is only one path in the + // matcher, sort by longer path first + if len(iPM) == 1 && len(jPM) == 1 { + return len(iPM[0]) > len(jPM[0]) + } + } + } return dirPositions[iDir] < dirPositions[jDir] }) } + + // add all the routes piled in from directives for _, r := range dirRoutes { + // as a special case, group rewrite directives so that they are mutually exclusive; + // this means that only the first matching rewrite will be evaluated, and that's + // probably a good thing, since there should never be a need to do more than one + // rewrite (I think?), and cascading rewrites smell bad... imagine these rewrites: + // rewrite /docs/json/* /docs/json/index.html + // rewrite /docs/* /docs/index.html + // (We use this on the Caddy website, or at least we did once.) The first rewrite's + // result is also matched by the second rewrite, making the first rewrite pointless. + // See issue #2959. + if r.directive == "rewrite" { + route := r.Value.(caddyhttp.Route) + route.Group = "rewriting" + r.Value = route + } + handlerSubroute.Routes = append(handlerSubroute.Routes, r.Value.(caddyhttp.Route)) } @@ -480,17 +541,16 @@ func matcherSetFromMatcherToken( if tkn.Text == "*" { // match all requests == no matchers, so nothing to do return nil, true, nil - } else if strings.HasPrefix(tkn.Text, "/") || strings.HasPrefix(tkn.Text, "=/") { + } else if strings.HasPrefix(tkn.Text, "/") { // convenient way to specify a single path match return caddy.ModuleMap{ "path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings), }, true, nil - } else if strings.HasPrefix(tkn.Text, "match:") { + } else if strings.HasPrefix(tkn.Text, matcherPrefix) { // pre-defined matcher - matcherName := strings.TrimPrefix(tkn.Text, "match:") - m, ok := matcherDefs[matcherName] + m, ok := matcherDefs[tkn.Text] if !ok { - return nil, false, fmt.Errorf("unrecognized matcher name: %+v", matcherName) + return nil, false, fmt.Errorf("unrecognized matcher name: %+v", tkn.Text) } return m, true, nil } @@ -577,35 +637,37 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([ return matcherSetsEnc, nil } -func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]caddy.ModuleMap, error) { - matchers := make(map[string]caddy.ModuleMap) +func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error { for d.Next() { definitionName := d.Val() + + if _, ok := matchers[definitionName]; ok { + return fmt.Errorf("matcher is defined more than once: %s", definitionName) + } + matchers[definitionName] = make(caddy.ModuleMap) + for nesting := d.Nesting(); d.NextBlock(nesting); { matcherName := d.Val() mod, err := caddy.GetModule("http.matchers." + matcherName) if err != nil { - return nil, fmt.Errorf("getting matcher module '%s': %v", matcherName, err) + return fmt.Errorf("getting matcher module '%s': %v", matcherName, err) } unm, ok := mod.New().(caddyfile.Unmarshaler) if !ok { - return nil, fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) + return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) } err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) if err != nil { - return nil, err + return err } rm, ok := unm.(caddyhttp.RequestMatcher) if !ok { - return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) - } - if _, ok := matchers[definitionName]; !ok { - matchers[definitionName] = make(caddy.ModuleMap) + return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) } matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) } } - return matchers, nil + return nil } func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.ModuleMap, error) { @@ -643,5 +705,7 @@ type sbAddrAssociation struct { serverBlocks []serverBlock } +const matcherPrefix = "@" + // Interface guard var _ caddyfile.ServerType = (*ServerType)(nil) diff --git a/caddyconfig/httpcaddyfile/parser_test.go b/caddyconfig/httpcaddyfile/parser_test.go index bcecf66..ae5751c 100644 --- a/caddyconfig/httpcaddyfile/parser_test.go +++ b/caddyconfig/httpcaddyfile/parser_test.go @@ -29,7 +29,7 @@ func TestParse(t *testing.T) { }{ { input: `http://localhost - matcher debug { + @debug { query showdebug=1 } `, @@ -38,7 +38,7 @@ func TestParse(t *testing.T) { }, { input: `http://localhost - matcher debug { + @debug { query bad format } `, diff --git a/cmd/main.go b/cmd/main.go index 564ef9f..d8e5f7b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -134,6 +134,13 @@ func loadConfig(configFile, adapterName string) ([]byte, error) { } } + // as a special case, if a config file called "Caddyfile" was + // specified, and no adapter is specified, assume caddyfile adapter + // for convenience + if filepath.Base(configFile) == "Caddyfile" && adapterName == "" { + adapterName = "caddyfile" + } + // load config adapter if adapterName != "" { cfgAdapter = caddyconfig.GetAdapter(adapterName) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 40a767f..f63e48e 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -46,7 +46,17 @@ type ( // [customized or disabled](/docs/json/apps/http/servers/automatic_https/). MatchHost []string - // MatchPath matches requests by the URI's path (case-insensitive). + // MatchPath matches requests by the URI's path (case-insensitive). Path + // matches are exact, but wildcards may be used: + // + // - At the end, for a prefix match (`/prefix/*`) + // - At the beginning, for a suffix match (`*.suffix`) + // - On both sides, for a substring match (`*/contains/*`) + // - In the middle, for a globular match (`/accounts/*/info`) + // + // This matcher is fast, so it does not support regular expressions or + // capture groups. For slower but more capable matching, use the path_regexp + // matcher. MatchPath []string // MatchPathRE matches requests by a regular expression on the URI's path. @@ -197,11 +207,15 @@ func (m MatchPath) Match(r *http.Request) bool { // being matched by *.php to be treated as PHP scripts lowerPath = strings.TrimRight(lowerPath, ". ") + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + for _, matchPath := range m { - // special case: first character is equals sign, - // treat it as an exact match - if strings.HasPrefix(matchPath, "=") { - if lowerPath == matchPath[1:] { + matchPath = repl.ReplaceAll(matchPath, "") + + // special case: first and last characters are wildcard, + // treat it as a fast substring match + if strings.HasPrefix(matchPath, "*") && strings.HasSuffix(matchPath, "*") { + if strings.Contains(lowerPath, matchPath[1:len(matchPath)-1]) { return true } continue @@ -216,15 +230,22 @@ func (m MatchPath) Match(r *http.Request) bool { continue } + // special case: last character is a wildcard, + // treat it as a fast prefix match + if strings.HasSuffix(matchPath, "*") { + if strings.HasPrefix(lowerPath, matchPath[:len(matchPath)-1]) { + return true + } + continue + } + + // for everything else, try globular matching, which also + // is exact matching if there are no glob/wildcard chars; // can ignore error here because we can't handle it anyway matches, _ := filepath.Match(matchPath, lowerPath) if matches { return true } - - if strings.HasPrefix(lowerPath, matchPath) { - return true - } } return false } diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index 34a1647..8e06546 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -183,8 +183,18 @@ func TestPathMatcher(t *testing.T) { expect: false, }, { + match: MatchPath{"/foo/bar/"}, + input: "/foo/bar/", + expect: true, + }, + { match: MatchPath{"/foo/bar/", "/other"}, input: "/other/", + expect: false, + }, + { + match: MatchPath{"/foo/bar/", "/other"}, + input: "/other", expect: true, }, { @@ -213,19 +223,19 @@ func TestPathMatcher(t *testing.T) { expect: false, }, { - match: MatchPath{"=/foo"}, - input: "/foo", + match: MatchPath{"*substring*"}, + input: "/foo/substring/bar.txt", expect: true, }, { - match: MatchPath{"=/foo"}, + match: MatchPath{"/foo"}, input: "/foo/bar", expect: false, }, { - match: MatchPath{"=/foo"}, - input: "/FOO", - expect: true, + match: MatchPath{"/foo"}, + input: "/foo/bar", + expect: false, }, { match: MatchPath{"/foo"}, @@ -233,12 +243,21 @@ func TestPathMatcher(t *testing.T) { expect: true, }, { + match: MatchPath{"/foo*"}, + input: "/FOOOO", + expect: true, + }, + { match: MatchPath{"/foo/bar.txt"}, input: "/foo/BAR.txt", expect: true, }, } { req := &http.Request{URL: &url.URL{Path: tc.input}} + repl := caddy.NewReplacer() + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + req = req.WithContext(ctx) + actual := tc.match.Match(req) if actual != tc.expect { t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) @@ -251,8 +270,13 @@ func TestPathMatcherWindows(t *testing.T) { // only Windows has this bug where it will ignore // trailing dots and spaces in a filename, but we // test for it on all platforms to be more consistent - match := MatchPath{"*.php"} + req := &http.Request{URL: &url.URL{Path: "/index.php . . .."}} + repl := caddy.NewReplacer() + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + req = req.WithContext(ctx) + + match := MatchPath{"*.php"} matched := match.Match(req) if !matched { t.Errorf("Expected to match; should ignore trailing dots and spaces") |