diff options
Diffstat (limited to 'caddyconfig/httpcaddyfile')
-rw-r--r-- | caddyconfig/httpcaddyfile/builtins.go | 34 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/directives.go | 124 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/httptype.go | 285 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/httptype_test.go | 33 | ||||
-rw-r--r-- | caddyconfig/httpcaddyfile/options.go | 87 |
5 files changed, 433 insertions, 130 deletions
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) { |