summaryrefslogtreecommitdiff
path: root/caddyconfig
diff options
context:
space:
mode:
Diffstat (limited to 'caddyconfig')
-rwxr-xr-xcaddyconfig/caddyfile/dispenser.go20
-rw-r--r--caddyconfig/httpcaddyfile/builtins.go34
-rw-r--r--caddyconfig/httpcaddyfile/directives.go124
-rw-r--r--caddyconfig/httpcaddyfile/httptype.go285
-rw-r--r--caddyconfig/httpcaddyfile/httptype_test.go33
-rw-r--r--caddyconfig/httpcaddyfile/options.go87
6 files changed, 444 insertions, 139 deletions
diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go
index 5b90b73..4ed9325 100755
--- a/caddyconfig/caddyfile/dispenser.go
+++ b/caddyconfig/caddyfile/dispenser.go
@@ -152,8 +152,10 @@ func (d *Dispenser) NextBlock(initialNestingLevel int) bool {
if !d.Next() {
return false // should be EOF error
}
- if d.Val() == "}" {
+ if d.Val() == "}" && !d.nextOnSameLine() {
d.nesting--
+ } else if d.Val() == "{" && !d.nextOnSameLine() {
+ d.nesting++
}
return d.nesting > initialNestingLevel
}
@@ -262,9 +264,9 @@ func (d *Dispenser) NewFromNextTokens() *Dispenser {
if !openedBlock {
// because NextBlock() consumes the initial open
// curly brace, we rewind here to append it, since
- // our case is special in that we want to include
- // all the tokens including surrounding curly braces
- // for a new dispenser to have
+ // our case is special in that we want the new
+ // dispenser to have all the tokens including
+ // surrounding curly braces
d.Prev()
tkns = append(tkns, d.Token())
d.Next()
@@ -273,12 +275,12 @@ func (d *Dispenser) NewFromNextTokens() *Dispenser {
tkns = append(tkns, d.Token())
}
if openedBlock {
- // include closing brace accordingly
+ // include closing brace
tkns = append(tkns, d.Token())
- // since NewFromNextTokens is intended to consume the entire
- // directive, we must call Next() here and consume the closing
- // curly brace
- d.Next()
+
+ // do not consume the closing curly brace; the
+ // next iteration of the enclosing loop will
+ // call Next() and consume it
}
return NewDispenser(tkns)
}
diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go
index ebb03cc..daba03b 100644
--- a/caddyconfig/httpcaddyfile/builtins.go
+++ b/caddyconfig/httpcaddyfile/builtins.go
@@ -34,6 +34,7 @@ func init() {
RegisterHandlerDirective("redir", parseRedir)
RegisterHandlerDirective("respond", parseRespond)
RegisterHandlerDirective("route", parseRoute)
+ RegisterHandlerDirective("handle", parseHandle)
}
func parseBind(h Helper) ([]ConfigValue, error) {
@@ -76,7 +77,7 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
route.MatcherSetsRaw = []caddy.ModuleMap{matcherSet}
}
- return h.NewVarsRoute(route), nil
+ return []ConfigValue{{Class: "route", Value: route}}, nil
}
func parseTLS(h Helper) ([]ConfigValue, error) {
@@ -330,3 +331,34 @@ func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
return sr, nil
}
+
+func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) {
+ var allResults []ConfigValue
+
+ 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 {
+ result.directive = dir
+ allResults = append(allResults, result)
+ }
+ }
+
+ return buildSubroute(allResults, h.groupCounter)
+ }
+
+ return nil, nil
+}
diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go
index 19ecb26..7acdb8c 100644
--- a/caddyconfig/httpcaddyfile/directives.go
+++ b/caddyconfig/httpcaddyfile/directives.go
@@ -16,6 +16,7 @@ package httpcaddyfile
import (
"encoding/json"
+ "sort"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -23,27 +24,45 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
-// defaultDirectiveOrder specifies the order
+// directiveOrder specifies the order
// to apply directives in HTTP routes.
-var defaultDirectiveOrder = []string{
+var directiveOrder = []string{
+ "root",
+
+ "redir",
"rewrite",
+
"strip_prefix",
"strip_suffix",
"uri_replace",
"try_files",
+
"basicauth",
"headers",
"request_header",
"encode",
"templates",
+
+ "handle",
"route",
- "redir",
+
"respond",
"reverse_proxy",
"php_fastcgi",
"file_server",
}
+// directiveIsOrdered returns true if dir is
+// a known, ordered (sorted) directive.
+func directiveIsOrdered(dir string) bool {
+ for _, d := range directiveOrder {
+ if d == dir {
+ return true
+ }
+ }
+ return false
+}
+
// RegisterDirective registers a unique directive dir with an
// associated unmarshaling (setup) function. When directive dir
// is encountered in a Caddyfile, setupFunc will be called to
@@ -92,10 +111,11 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
// Caddyfile tokens.
type Helper struct {
*caddyfile.Dispenser
- options map[string]interface{}
- warnings *[]caddyconfig.Warning
- matcherDefs map[string]caddy.ModuleMap
- parentBlock caddyfile.ServerBlock
+ options map[string]interface{}
+ warnings *[]caddyconfig.Warning
+ matcherDefs map[string]caddy.ModuleMap
+ parentBlock caddyfile.ServerBlock
+ groupCounter counter
}
// Option gets the option keyed by name.
@@ -127,8 +147,8 @@ func (h Helper) JSON(val interface{}) json.RawMessage {
return caddyconfig.JSON(val, h.warnings)
}
-// MatcherToken assumes the current token is (possibly) a matcher, and
-// if so, returns the matcher set along with a true value. If the current
+// MatcherToken assumes the next argument token is (possibly) a matcher,
+// and if so, returns the matcher set along with a true value. If the next
// token is not a matcher, nil and false is returned. Note that a true
// value may be returned with a nil matcher set if it is a catch-all.
func (h Helper) MatcherToken() (caddy.ModuleMap, bool, error) {
@@ -165,18 +185,39 @@ func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
}
}
+// GroupRoutes adds the routes (caddyhttp.Route type) in vals to the
+// same group, if there is more than one route in vals.
+func (h Helper) GroupRoutes(vals []ConfigValue) {
+ // ensure there's at least two routes; group of one is pointless
+ var count int
+ for _, v := range vals {
+ if _, ok := v.Value.(caddyhttp.Route); ok {
+ count++
+ if count > 1 {
+ break
+ }
+ }
+ }
+ if count < 2 {
+ return
+ }
+
+ // now that we know the group will have some effect, do it
+ groupName := h.groupCounter.nextGroup()
+ for i := range vals {
+ if route, ok := vals[i].Value.(caddyhttp.Route); ok {
+ route.Group = groupName
+ vals[i].Value = route
+ }
+ }
+}
+
// NewBindAddresses returns config values relevant to adding
// listener bind addresses to the config.
func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
return []ConfigValue{{Class: "bind", Value: addrs}}
}
-// NewVarsRoute returns config values relevant to adding a
-// "vars" wrapper route to the config.
-func (h Helper) NewVarsRoute(route caddyhttp.Route) []ConfigValue {
- return []ConfigValue{{Class: "var", Value: route}}
-}
-
// ConfigValue represents a value to be added to the final
// configuration, or a value to be consulted when building
// the final configuration.
@@ -197,6 +238,59 @@ type ConfigValue struct {
directive string
}
+func sortRoutes(routes []ConfigValue) {
+ dirPositions := make(map[string]int)
+ for i, dir := range directiveOrder {
+ dirPositions[dir] = i
+ }
+
+ // while we are sorting, we will need to decode a route's path matcher
+ // in order to sub-sort by path length; we can amortize this operation
+ // for efficiency by storing the decoded matchers in a slice
+ decodedMatchers := make([]caddyhttp.MatchPath, len(routes))
+
+ sort.SliceStable(routes, func(i, j int) bool {
+ iDir, jDir := routes[i].directive, routes[j].directive
+ if iDir == jDir {
+ // directives are the same; sub-sort by path matcher length
+ // if there's only one matcher set and one path (common case)
+ iRoute, ok := routes[i].Value.(caddyhttp.Route)
+ if !ok {
+ return false
+ }
+ jRoute, ok := routes[j].Value.(caddyhttp.Route)
+ if !ok {
+ return false
+ }
+
+ if len(iRoute.MatcherSetsRaw) == 1 && len(jRoute.MatcherSetsRaw) == 1 {
+ // use already-decoded matcher, or decode if it's the first time seeing it
+ iPM, jPM := decodedMatchers[i], decodedMatchers[j]
+ if iPM == nil {
+ var pathMatcher caddyhttp.MatchPath
+ _ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &pathMatcher)
+ decodedMatchers[i] = pathMatcher
+ iPM = pathMatcher
+ }
+ if jPM == nil {
+ var pathMatcher caddyhttp.MatchPath
+ _ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &pathMatcher)
+ decodedMatchers[j] = pathMatcher
+ jPM = pathMatcher
+ }
+
+ // if there is only one path in the matcher, sort by
+ // longer path (more specific) first
+ if len(iPM) == 1 && len(jPM) == 1 {
+ return len(iPM[0]) > len(jPM[0])
+ }
+ }
+ }
+
+ return dirPositions[iDir] < dirPositions[jDir]
+ })
+}
+
// serverBlock pairs a Caddyfile server block
// with a "pile" of config values, keyed by class
// name.
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index bec5045..a57b6e9 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -41,6 +41,7 @@ type ServerType struct {
func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
options map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error) {
var warnings []caddyconfig.Warning
+ gc := counter{new(int)}
var serverBlocks []serverBlock
for _, sblock := range originalServerBlocks {
@@ -64,8 +65,8 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
val, err = parseOptHTTPPort(disp)
case "https_port":
val, err = parseOptHTTPSPort(disp)
- case "handler_order":
- val, err = parseOptHandlerOrder(disp)
+ case "order":
+ val, err = parseOptOrder(disp)
case "experimental_http3":
val, err = parseOptExperimentalHTTP3(disp)
case "storage":
@@ -140,11 +141,12 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
}
results, err := dirFunc(Helper{
- Dispenser: caddyfile.NewDispenser(segment),
- options: options,
- warnings: &warnings,
- matcherDefs: matcherDefs,
- parentBlock: sb.block,
+ Dispenser: caddyfile.NewDispenser(segment),
+ options: options,
+ warnings: &warnings,
+ matcherDefs: matcherDefs,
+ parentBlock: sb.block,
+ groupCounter: gc,
})
if err != nil {
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
@@ -167,7 +169,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
// each pairing of listener addresses to list of server
// blocks is basically a server definition
- servers, err := st.serversFromPairings(pairings, options, &warnings)
+ servers, err := st.serversFromPairings(pairings, options, &warnings, gc)
if err != nil {
return nil, warnings, err
}
@@ -304,6 +306,7 @@ func (st *ServerType) serversFromPairings(
pairings []sbAddrAssociation,
options map[string]interface{},
warnings *[]caddyconfig.Warning,
+ groupCounter counter,
) (map[string]*caddyhttp.Server, error) {
servers := make(map[string]*caddyhttp.Server)
@@ -312,16 +315,48 @@ func (st *ServerType) serversFromPairings(
Listen: p.addresses,
}
+ // sort server blocks by their keys; this is important because
+ // only the first matching site should be evaluated, and we should
+ // attempt to match most specific site first (host and path), in
+ // case their matchers overlap; we do this somewhat naively by
+ // descending sort by length of host then path
+ sort.SliceStable(p.serverBlocks, func(i, j int) bool {
+ // TODO: we could pre-process the specificities for efficiency,
+ // but I don't expect many blocks will have SO many keys...
+ var iLongestPath, jLongestPath string
+ var iLongestHost, jLongestHost string
+ for _, key := range p.serverBlocks[i].block.Keys {
+ addr, _ := ParseAddress(key)
+ if specificity(addr.Host) > specificity(iLongestHost) {
+ iLongestHost = addr.Host
+ }
+ if specificity(addr.Path) > specificity(iLongestPath) {
+ iLongestPath = addr.Path
+ }
+ }
+ for _, key := range p.serverBlocks[j].block.Keys {
+ addr, _ := ParseAddress(key)
+ if specificity(addr.Host) > specificity(jLongestHost) {
+ jLongestHost = addr.Host
+ }
+ if specificity(addr.Path) > specificity(jLongestPath) {
+ jLongestPath = addr.Path
+ }
+ }
+ if specificity(iLongestHost) == specificity(jLongestHost) {
+ return len(iLongestPath) > len(jLongestPath)
+ }
+ return specificity(iLongestHost) > specificity(jLongestHost)
+ })
+
+ // create a subroute for each site in the server block
for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock.block)
if err != nil {
return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err)
}
- siteSubroute := new(caddyhttp.Subroute)
-
// tls: connection policies and toggle auto HTTPS
-
autoHTTPSQualifiedHosts, err := st.autoHTTPSHosts(sblock)
if err != nil {
return nil, err
@@ -354,105 +389,126 @@ func (st *ServerType) serversFromPairings(
// TODO: consolidate equal conn policies
}
- // vars: make sure these are linked in first so future
- // routes can use the variables they define
- for _, cfgVal := range sblock.pile["var"] {
- siteSubroute.Routes = append(siteSubroute.Routes, cfgVal.Value.(caddyhttp.Route))
- }
-
- // set up each handler directive - the order of the handlers
- // as they are added to the routes depends on user preference
+ // set up each handler directive, making sure to honor directive order
dirRoutes := sblock.pile["route"]
- handlerOrder, ok := options["handler_order"].([]string)
- if !ok {
- handlerOrder = defaultDirectiveOrder
- }
- if len(handlerOrder) == 1 && handlerOrder[0] == "appearance" {
- handlerOrder = nil
+ siteSubroute, err := buildSubroute(dirRoutes, groupCounter)
+ if err != nil {
+ return nil, err
}
- if handlerOrder != nil {
- dirPositions := make(map[string]int)
- for i, dir := range handlerOrder {
- dirPositions[dir] = i
- }
- 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]
+
+ if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
+ // no need to wrap the handlers in a subroute if this is
+ // the only server block and there is no matcher for it
+ srv.Routes = append(srv.Routes, siteSubroute.Routes...)
+ } else {
+ srv.Routes = append(srv.Routes, caddyhttp.Route{
+ MatcherSetsRaw: matcherSetsEnc,
+ HandlersRaw: []json.RawMessage{
+ caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings),
+ },
+ Terminal: true, // only first matching site block should be evaluated
})
}
+ }
- // 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
- }
+ srv.Routes = consolidateRoutes(srv.Routes)
+
+ servers[fmt.Sprintf("srv%d", i)] = srv
+ }
+
+ return servers, nil
+}
+
+func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subroute, error) {
+ for _, val := range routes {
+ if !directiveIsOrdered(val.directive) {
+ return nil, fmt.Errorf("directive '%s' is not ordered, so it cannot be used here", val.directive)
+ }
+ }
+
+ sortRoutes(routes)
+
+ subroute := new(caddyhttp.Subroute)
- siteSubroute.Routes = append(siteSubroute.Routes, r.Value.(caddyhttp.Route))
+ // get a group name for rewrite directives, if needed
+ var rewriteGroupName string
+ var rewriteCount int
+ for _, r := range routes {
+ if r.directive == "rewrite" {
+ rewriteCount++
+ if rewriteCount > 1 {
+ break
}
+ }
+ }
+ if rewriteCount > 1 {
+ rewriteGroupName = groupCounter.nextGroup()
+ }
- siteSubroute.Routes = consolidateRoutes(siteSubroute.Routes)
+ // get a group name for handle blocks, if needed
+ var handleGroupName string
+ var handleCount int
+ for _, r := range routes {
+ if r.directive == "handle" {
+ handleCount++
+ if handleCount > 1 {
+ break
+ }
+ }
+ }
+ if handleCount > 1 {
+ handleGroupName = groupCounter.nextGroup()
+ }
- srv.Routes = append(srv.Routes, caddyhttp.Route{
- MatcherSetsRaw: matcherSetsEnc,
- HandlersRaw: []json.RawMessage{
- caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings),
- },
- })
+ // add all the routes piled in from directives
+ for _, r := range routes {
+ // 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 = rewriteGroupName
+ r.Value = route
}
- srv.Routes = consolidateRoutes(srv.Routes)
+ // handle blocks are also mutually exclusive by definition
+ if r.directive == "handle" {
+ route := r.Value.(caddyhttp.Route)
+ route.Group = handleGroupName
+ r.Value = route
+ }
- servers[fmt.Sprintf("srv%d", i)] = srv
+ switch route := r.Value.(type) {
+ case caddyhttp.Subroute:
+ // if a route-class config value is actually a Subroute handler
+ // with nothing but a list of routes, then it is the intention
+ // of the directive to keep these handlers together and in this
+ // same order, but not necessarily in a subroute (if it wanted
+ // to keep them in a subroute, the directive would have returned
+ // a route with a Subroute as its handler); this is useful to
+ // keep multiple handlers/routes together and in the same order
+ // so that the sorting procedure we did above doesn't reorder them
+ if route.Errors != nil {
+ // if error handlers are also set, this is confusing; it's
+ // probably supposed to be wrapped in a Route and encoded
+ // as a regular handler route... programmer error.
+ panic("found subroute with more than just routes; perhaps it should have been wrapped in a route?")
+ }
+ subroute.Routes = append(subroute.Routes, route.Routes...)
+ case caddyhttp.Route:
+ subroute.Routes = append(subroute.Routes, route)
+ }
}
- return servers, nil
+ subroute.Routes = consolidateRoutes(subroute.Routes)
+
+ return subroute, nil
}
func (st ServerType) autoHTTPSHosts(sb serverBlock) ([]string, error) {
@@ -530,7 +586,6 @@ func matcherSetFromMatcherToken(
}
return m, true, nil
}
-
return nil, false, nil
}
@@ -668,6 +723,42 @@ func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
return intVal
}
+// specifity returns len(s) minus any wildcards (*) and
+// placeholders ({...}). Basically, it's a length count
+// that penalizes the use of wildcards and placeholders.
+// This is useful for comparing hostnames and paths.
+// However, wildcards in paths are not a sure answer to
+// the question of specificity. For exmaple,
+// '*.example.com' is clearly less specific than
+// 'a.example.com', but is '/a' more or less specific
+// than '/a*'?
+func specificity(s string) int {
+ l := len(s) - strings.Count(s, "*")
+ for len(s) > 0 {
+ start := strings.Index(s, "{")
+ if start < 0 {
+ return l
+ }
+ end := strings.Index(s[start:], "}") + start + 1
+ if end <= start {
+ return l
+ }
+ l -= end - start
+ s = s[end:]
+ }
+ return l
+}
+
+type counter struct {
+ n *int
+}
+
+func (c counter) nextGroup() string {
+ name := fmt.Sprintf("group%d", *c.n)
+ *c.n++
+ return name
+}
+
type matcherSetAndTokens struct {
matcherSet caddy.ModuleMap
tokens []caddyfile.Token
diff --git a/caddyconfig/httpcaddyfile/httptype_test.go b/caddyconfig/httpcaddyfile/httptype_test.go
new file mode 100644
index 0000000..ae4f042
--- /dev/null
+++ b/caddyconfig/httpcaddyfile/httptype_test.go
@@ -0,0 +1,33 @@
+package httpcaddyfile
+
+import "testing"
+
+func TestSpecificity(t *testing.T) {
+ for i, tc := range []struct {
+ input string
+ expect int
+ }{
+ {"", 0},
+ {"*", 0},
+ {"*.*", 1},
+ {"{placeholder}", 0},
+ {"/{placeholder}", 1},
+ {"foo", 3},
+ {"example.com", 11},
+ {"a.example.com", 13},
+ {"*.example.com", 12},
+ {"/foo", 4},
+ {"/foo*", 4},
+ {"{placeholder}.example.com", 12},
+ {"{placeholder.example.com", 24},
+ {"}.", 2},
+ {"}{", 2},
+ {"{}", 0},
+ {"{{{}}", 1},
+ } {
+ actual := specificity(tc.input)
+ if actual != tc.expect {
+ t.Errorf("Test %d (%s): Expected %d but got %d", i, tc.input, tc.expect, actual)
+ }
+ }
+}
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index e87b30f..e81528e 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -58,27 +58,80 @@ func parseOptExperimentalHTTP3(d *caddyfile.Dispenser) (bool, error) {
return true, nil
}
-func parseOptHandlerOrder(d *caddyfile.Dispenser) ([]string, error) {
- if !d.Next() {
- return nil, d.ArgErr()
- }
- order := d.RemainingArgs()
- if len(order) == 1 && order[0] == "appearance" {
- return []string{"appearance"}, nil
- }
- if len(order) > 0 && d.NextBlock(0) {
- return nil, d.Err("cannot open block if there are arguments")
- }
- for d.NextBlock(0) {
- order = append(order, d.Val())
+func parseOptOrder(d *caddyfile.Dispenser) ([]string, error) {
+ newOrder := directiveOrder
+
+ for d.Next() {
+ // get directive name
+ if !d.Next() {
+ return nil, d.ArgErr()
+ }
+ dirName := d.Val()
+ if _, ok := registeredDirectives[dirName]; !ok {
+ return nil, fmt.Errorf("%s is not a registered directive", dirName)
+ }
+
+ // get positional token
+ if !d.Next() {
+ return nil, d.ArgErr()
+ }
+ pos := d.Val()
+
+ // if directive exists, first remove it
+ for i, d := range newOrder {
+ if d == dirName {
+ newOrder = append(newOrder[:i], newOrder[i+1:]...)
+ break
+ }
+ }
+
+ // act on the positional
+ switch pos {
+ case "first":
+ newOrder = append([]string{dirName}, newOrder...)
+ if d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ directiveOrder = newOrder
+ return newOrder, nil
+ case "last":
+ newOrder = append(newOrder, dirName)
+ if d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ directiveOrder = newOrder
+ return newOrder, nil
+ case "before":
+ case "after":
+ default:
+ return nil, fmt.Errorf("unknown positional '%s'", pos)
+ }
+
+ // get name of other directive
+ if !d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ otherDir := d.Val()
if d.NextArg() {
return nil, d.ArgErr()
}
+
+ // insert directive into proper position
+ for i, d := range newOrder {
+ if d == otherDir {
+ if pos == "before" {
+ newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
+ } else if pos == "after" {
+ newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
+ }
+ break
+ }
+ }
}
- if len(order) == 0 {
- return nil, d.ArgErr()
- }
- return order, nil
+
+ directiveOrder = newOrder
+
+ return newOrder, nil
}
func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) {