fileserver: Add first_exist_fallback strategy for try_files (#6699)

* feat: add first_exist_or_fallback strategy for try_files

* fix tests

* linter
This commit is contained in:
Kévin Dunglas 2024-12-03 13:44:49 +01:00 committed by GitHub
parent b116dcea3d
commit efd9251ad3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 40 additions and 14 deletions

View file

@ -58,6 +58,7 @@
"{http.request.uri.path}/index.php", "{http.request.uri.path}/index.php",
"index.php" "index.php"
], ],
"try_policy": "first_exist_fallback",
"split_path": [ "split_path": [
".php" ".php"
] ]

View file

@ -73,7 +73,8 @@ php_fastcgi @test localhost:9000
"{http.request.uri.path}", "{http.request.uri.path}",
"{http.request.uri.path}/index.php", "{http.request.uri.path}/index.php",
"index.php" "index.php"
] ],
"try_policy": "first_exist_fallback"
} }
} }
] ]

View file

@ -59,6 +59,7 @@ php_fastcgi localhost:9000 {
"{http.request.uri.path}/index.php5", "{http.request.uri.path}/index.php5",
"index.php5" "index.php5"
], ],
"try_policy": "first_exist_fallback",
"split_path": [ "split_path": [
".php", ".php",
".php5" ".php5"

View file

@ -32,6 +32,7 @@ php_fastcgi localhost:9000 {
"{http.request.uri.path}", "{http.request.uri.path}",
"index.php" "index.php"
], ],
"try_policy": "first_exist_fallback",
"split_path": [ "split_path": [
".php", ".php",
".php5" ".php5"

View file

@ -274,7 +274,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
tryPolicy = h.Val() tryPolicy = h.Val()
switch tryPolicy { switch tryPolicy {
case tryPolicyFirstExist, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod: case tryPolicyFirstExist, tryPolicyFirstExistFallback, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
default: default:
return nil, h.Errf("unrecognized try policy: %s", tryPolicy) return nil, h.Errf("unrecognized try policy: %s", tryPolicy)
} }

View file

@ -90,6 +90,7 @@ type MatchFile struct {
// How to choose a file in TryFiles. Can be: // How to choose a file in TryFiles. Can be:
// //
// - first_exist // - first_exist
// - first_exist_fallback
// - smallest_size // - smallest_size
// - largest_size // - largest_size
// - most_recently_modified // - most_recently_modified
@ -415,13 +416,13 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
} }
// setPlaceholders creates the placeholders for the matched file // setPlaceholders creates the placeholders for the matched file
setPlaceholders := func(candidate matchCandidate, info fs.FileInfo) { setPlaceholders := func(candidate matchCandidate, isDir bool) {
repl.Set("http.matchers.file.relative", filepath.ToSlash(candidate.relative)) repl.Set("http.matchers.file.relative", filepath.ToSlash(candidate.relative))
repl.Set("http.matchers.file.absolute", filepath.ToSlash(candidate.fullpath)) repl.Set("http.matchers.file.absolute", filepath.ToSlash(candidate.fullpath))
repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder)) repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder))
fileType := "file" fileType := "file"
if info.IsDir() { if isDir {
fileType = "directory" fileType = "directory"
} }
repl.Set("http.matchers.file.type", fileType) repl.Set("http.matchers.file.type", fileType)
@ -429,8 +430,13 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
// match file according to the configured policy // match file according to the configured policy
switch m.TryPolicy { switch m.TryPolicy {
case "", tryPolicyFirstExist: case "", tryPolicyFirstExist, tryPolicyFirstExistFallback:
for _, pattern := range m.TryFiles { maxI := -1
if m.TryPolicy == tryPolicyFirstExistFallback {
maxI = len(m.TryFiles) - 1
}
for i, pattern := range m.TryFiles {
// If the pattern is a status code, emit an error, // If the pattern is a status code, emit an error,
// which short-circuits the middleware pipeline and // which short-circuits the middleware pipeline and
// writes an HTTP error response. // writes an HTTP error response.
@ -440,8 +446,15 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
candidates := makeCandidates(pattern) candidates := makeCandidates(pattern)
for _, c := range candidates { for _, c := range candidates {
// Skip the IO if using fallback policy and it's the latest item
if i == maxI {
setPlaceholders(c, false)
return true, nil
}
if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists { if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists {
setPlaceholders(c, info) setPlaceholders(c, info.IsDir())
return true, nil return true, nil
} }
} }
@ -465,7 +478,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
if largestInfo == nil { if largestInfo == nil {
return false, nil return false, nil
} }
setPlaceholders(largest, largestInfo) setPlaceholders(largest, largestInfo.IsDir())
return true, nil return true, nil
case tryPolicySmallestSize: case tryPolicySmallestSize:
@ -486,7 +499,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
if smallestInfo == nil { if smallestInfo == nil {
return false, nil return false, nil
} }
setPlaceholders(smallest, smallestInfo) setPlaceholders(smallest, smallestInfo.IsDir())
return true, nil return true, nil
case tryPolicyMostRecentlyMod: case tryPolicyMostRecentlyMod:
@ -506,7 +519,7 @@ func (m MatchFile) selectFile(r *http.Request) (bool, error) {
if recentInfo == nil { if recentInfo == nil {
return false, nil return false, nil
} }
setPlaceholders(recent, recentInfo) setPlaceholders(recent, recentInfo.IsDir())
return true, nil return true, nil
} }
@ -708,10 +721,11 @@ var globSafeRepl = strings.NewReplacer(
) )
const ( const (
tryPolicyFirstExist = "first_exist" tryPolicyFirstExist = "first_exist"
tryPolicyLargestSize = "largest_size" tryPolicyFirstExistFallback = "first_exist_fallback"
tryPolicySmallestSize = "smallest_size" tryPolicyLargestSize = "largest_size"
tryPolicyMostRecentlyMod = "most_recently_modified" tryPolicySmallestSize = "smallest_size"
tryPolicyMostRecentlyMod = "most_recently_modified"
) )
// Interface guards // Interface guards

View file

@ -18,6 +18,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
@ -312,12 +313,18 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
if indexFile != "off" { if indexFile != "off" {
dirRedir := false dirRedir := false
dirIndex := "{http.request.uri.path}/" + indexFile dirIndex := "{http.request.uri.path}/" + indexFile
tryPolicy := "first_exist_fallback"
// if tryFiles wasn't overridden, use a reasonable default // if tryFiles wasn't overridden, use a reasonable default
if len(tryFiles) == 0 { if len(tryFiles) == 0 {
tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile} tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile}
dirRedir = true dirRedir = true
} else { } else {
if !strings.HasSuffix(tryFiles[len(tryFiles)-1], ".php") {
// use first_exist strategy if the last file is not a PHP file
tryPolicy = ""
}
for _, tf := range tryFiles { for _, tf := range tryFiles {
if tf == dirIndex { if tf == dirIndex {
dirRedir = true dirRedir = true
@ -357,6 +364,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
rewriteMatcherSet := caddy.ModuleMap{ rewriteMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{ "file": h.JSON(fileserver.MatchFile{
TryFiles: tryFiles, TryFiles: tryFiles,
TryPolicy: tryPolicy,
SplitPath: extensions, SplitPath: extensions,
}), }),
} }