@marcel91200 / caddy Mirror · read-only / commits / a66f4612
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

caddyfile: Sort site subroutes by key specificity, and make exclusive

In the v1 Caddyfile, only the first matching site definition would be used, so setting these `Terminal: true` ensures that only the first matching one is used in v2, too. We also have to sort by key specificity... Caddy 1 had a special data structure for selecting the most specific site definition, but we don't have that structure in v2, so we need to sort by length (of host and path, separately). For blocks where more than one key is present, we choose the longest host and path (independently, need not be from same key) by which to sort.

Matthew Holt committed Jan 15, 2020 at 13:51 UTC a66f461201ae2b7e724b3067f74019bdf44f834e
2 files changed +102 -46
caddyconfig/httpcaddyfile/directives.go
+43
@@ -16,6 +16,7 @@ package httpcaddyfile
16
17 import (
18 "encoding/json"
19 + "sort"
20
21 "github.com/caddyserver/caddy/v2"
22 "github.com/caddyserver/caddy/v2/caddyconfig"
@@ -197,6 +198,48 @@ type ConfigValue struct {
198 directive string
199 }
200
201 +func sortRoutes(handlers []ConfigValue, dirPositions map[string]int) {
202 + // while we are sorting, we will need to decode a route's path matcher
203 + // in order to sub-sort by path length; we can amortize this operation
204 + // for efficiency by storing the decoded matchers in a slice
205 + decodedMatchers := make([]caddyhttp.MatchPath, len(handlers))
206 +
207 + sort.SliceStable(handlers, func(i, j int) bool {
208 + iDir, jDir := handlers[i].directive, handlers[j].directive
209 + if iDir == jDir {
210 + // directives are the same; sub-sort by path matcher length
211 + // if there's only one matcher set and one path (common case)
212 + iRoute := handlers[i].Value.(caddyhttp.Route)
213 + jRoute := handlers[j].Value.(caddyhttp.Route)
214 +
215 + if len(iRoute.MatcherSetsRaw) == 1 && len(jRoute.MatcherSetsRaw) == 1 {
216 + // use already-decoded matcher, or decode if it's the first time seeing it
217 + iPM, jPM := decodedMatchers[i], decodedMatchers[j]
218 + if iPM == nil {
219 + var pathMatcher caddyhttp.MatchPath
220 + _ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &pathMatcher)
221 + decodedMatchers[i] = pathMatcher
222 + iPM = pathMatcher
223 + }
224 + if jPM == nil {
225 + var pathMatcher caddyhttp.MatchPath
226 + _ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &pathMatcher)
227 + decodedMatchers[j] = pathMatcher
228 + jPM = pathMatcher
229 + }
230 +
231 + // if there is only one path in the matcher, sort by
232 + // longer path (more specific) first
233 + if len(iPM) == 1 && len(jPM) == 1 {
234 + return len(iPM[0]) > len(jPM[0])
235 + }
236 + }
237 + }
238 +
239 + return dirPositions[iDir] < dirPositions[jDir]
240 + })
241 +}
242 +
243 // serverBlock pairs a Caddyfile server block
244 // with a "pile" of config values, keyed by class
245 // name.
caddyconfig/httpcaddyfile/httptype.go
+59 -46
@@ -312,6 +312,41 @@ func (st *ServerType) serversFromPairings(
312 Listen: p.addresses,
313 }
314
315 + // sort server blocks by their keys; this is important because
316 + // only the first matching site should be evaluated, and we should
317 + // attempt to match most specific site first (host and path), in
318 + // case their matchers overlap; we do this somewhat naively by
319 + // descending sort by length of host then path
320 + sort.SliceStable(p.serverBlocks, func(i, j int) bool {
321 + // TODO: we could pre-process the lengths for efficiency,
322 + // but I don't expect many blocks will have SO many keys...
323 + var iLongestPath, jLongestPath string
324 + var iLongestHost, jLongestHost string
325 + for _, key := range p.serverBlocks[i].block.Keys {
326 + addr, _ := ParseAddress(key)
327 + if length(addr.Host) > length(iLongestHost) {
328 + iLongestHost = addr.Host
329 + }
330 + if len(addr.Path) > len(iLongestPath) {
331 + iLongestPath = addr.Path
332 + }
333 + }
334 + for _, key := range p.serverBlocks[j].block.Keys {
335 + addr, _ := ParseAddress(key)
336 + if length(addr.Host) > length(jLongestHost) {
337 + jLongestHost = addr.Host
338 + }
339 + if len(addr.Path) > len(jLongestPath) {
340 + jLongestPath = addr.Path
341 + }
342 + }
343 + if length(iLongestHost) == length(jLongestHost) {
344 + return len(iLongestPath) > len(jLongestPath)
345 + }
346 + return length(iLongestHost) > length(jLongestHost)
347 + })
348 +
349 + // create a subroute for each site in the server block
350 for _, sblock := range p.serverBlocks {
351 matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock.block)
352 if err != nil {
@@ -375,46 +410,7 @@ func (st *ServerType) serversFromPairings(
410 for i, dir := range handlerOrder {
411 dirPositions[dir] = i
412 }
378 - sort.SliceStable(dirRoutes, func(i, j int) bool {
379 - iDir, jDir := dirRoutes[i].directive, dirRoutes[j].directive
380 - if iDir == jDir {
381 - // TODO: we really need to refactor this into a separate function or method...
382 - // sub-sort by path matcher length, if there's only one
383 - iRoute := dirRoutes[i].Value.(caddyhttp.Route)
384 - jRoute := dirRoutes[j].Value.(caddyhttp.Route)
385 - if len(iRoute.MatcherSetsRaw) == 1 && len(jRoute.MatcherSetsRaw) == 1 {
386 - // for slightly better efficiency, only decode the path matchers once,
387 - // then just store them arbitrarily in the decoded MatcherSets field,
388 - // ours should be the only thing in there
389 - var iPM, jPM caddyhttp.MatchPath
390 - if len(iRoute.MatcherSets) == 1 {
391 - iPM = iRoute.MatcherSets[0][0].(caddyhttp.MatchPath)
392 - }
393 - if len(jRoute.MatcherSets) == 1 {
394 - jPM = jRoute.MatcherSets[0][0].(caddyhttp.MatchPath)
395 - }
396 - // if it's our first time seeing this route's path matcher, decode it
397 - if iPM == nil {
398 - var pathMatcher caddyhttp.MatchPath
399 - _ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &pathMatcher)
400 - iRoute.MatcherSets = caddyhttp.MatcherSets{{pathMatcher}}
401 - iPM = pathMatcher
402 - }
403 - if jPM == nil {
404 - var pathMatcher caddyhttp.MatchPath
405 - _ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &pathMatcher)
406 - jRoute.MatcherSets = caddyhttp.MatcherSets{{pathMatcher}}
407 - jPM = pathMatcher
408 - }
409 - // finally, if there is only one path in the
410 - // matcher, sort by longer path first
411 - if len(iPM) == 1 && len(jPM) == 1 {
412 - return len(iPM[0]) > len(jPM[0])
413 - }
414 - }
415 - }
416 - return dirPositions[iDir] < dirPositions[jDir]
417 - })
413 + sortRoutes(dirRoutes, dirPositions)
414 }
415
416 // add all the routes piled in from directives
@@ -439,12 +435,19 @@ func (st *ServerType) serversFromPairings(
435
436 siteSubroute.Routes = consolidateRoutes(siteSubroute.Routes)
437
442 - srv.Routes = append(srv.Routes, caddyhttp.Route{
443 - MatcherSetsRaw: matcherSetsEnc,
444 - HandlersRaw: []json.RawMessage{
445 - caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings),
446 - },
447 - })
438 + if len(matcherSetsEnc) == 0 && len(p.serverBlocks) == 1 {
439 + // no need to wrap the handlers in a subroute if this is
440 + // the only server block and there is no matcher for it
441 + srv.Routes = append(srv.Routes, siteSubroute.Routes...)
442 + } else {
443 + srv.Routes = append(srv.Routes, caddyhttp.Route{
444 + MatcherSetsRaw: matcherSetsEnc,
445 + HandlersRaw: []json.RawMessage{
446 + caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings),
447 + },
448 + Terminal: true, // only first matching site block should be evaluated
449 + })
450 + }
451 }
452
453 srv.Routes = consolidateRoutes(srv.Routes)
@@ -668,6 +671,16 @@ func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
671 return intVal
672 }
673
674 +// length returns len(s) minus any wildcards (*). Basically,
675 +// it's a length count that penalizes the use of wildcards.
676 +// This is useful for comparing hostnames, but probably not
677 +// paths so much (for example, '*.example.com' is clearly
678 +// less specific than 'a.example.com', but is '/a' more or
679 +// less specific than '/a*'?).
680 +func length(s string) int {
681 + return len(s) - strings.Count(s, "*")
682 +}
683 +
684 type matcherSetAndTokens struct {
685 matcherSet caddy.ModuleMap
686 tokens []caddyfile.Token