aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/caddyhttp/fileserver
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp/fileserver')
-rw-r--r--modules/caddyhttp/fileserver/browse.go13
-rw-r--r--modules/caddyhttp/fileserver/caddyfile.go29
-rw-r--r--modules/caddyhttp/fileserver/command.go4
-rw-r--r--modules/caddyhttp/fileserver/matcher.go86
-rw-r--r--modules/caddyhttp/fileserver/matcher_test.go37
-rw-r--r--modules/caddyhttp/fileserver/staticfiles.go6
-rw-r--r--modules/caddyhttp/fileserver/testdata/large.txt3
7 files changed, 133 insertions, 45 deletions
diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go
index a19b4e17a..0a623c79e 100644
--- a/modules/caddyhttp/fileserver/browse.go
+++ b/modules/caddyhttp/fileserver/browse.go
@@ -66,8 +66,15 @@ type Browse struct {
// - `sort size` will sort by size in ascending order
// The first option must be `sort_by` and the second option must be `order` (if exists).
SortOptions []string `json:"sort,omitempty"`
+
+ // FileLimit limits the number of up to n DirEntry values in directory order.
+ FileLimit int `json:"file_limit,omitempty"`
}
+const (
+ defaultDirEntryLimit = 10000
+)
+
func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
if c := fsrv.logger.Check(zapcore.DebugLevel, "browse enabled; listing directory contents"); c != nil {
c.Write(zap.String("path", dirPath), zap.String("root", root))
@@ -206,7 +213,11 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
}
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs.FS, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) {
- files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
+ dirLimit := defaultDirEntryLimit
+ if fsrv.Browse.FileLimit != 0 {
+ dirLimit = fsrv.Browse.FileLimit
+ }
+ files, err := dir.ReadDir(dirLimit)
if err != nil && err != io.EOF {
return nil, err
}
diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go
index 71b7638e4..80a37322b 100644
--- a/modules/caddyhttp/fileserver/caddyfile.go
+++ b/modules/caddyhttp/fileserver/caddyfile.go
@@ -16,6 +16,7 @@ package fileserver
import (
"path/filepath"
+ "strconv"
"strings"
"github.com/caddyserver/caddy/v2"
@@ -78,7 +79,7 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.ArgErr()
}
- for d.NextBlock(0) {
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "fs":
if !d.NextArg() {
@@ -129,15 +130,29 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.Errf("unknown sort option '%s'", dVal)
}
}
+ case "file_limit":
+ fileLimit := d.RemainingArgs()
+ if len(fileLimit) != 1 {
+ return d.Err("file_limit should have an integer value")
+ }
+ val, _ := strconv.Atoi(fileLimit[0])
+ if fsrv.Browse.FileLimit != 0 {
+ return d.Err("file_limit is already enabled")
+ }
+ fsrv.Browse.FileLimit = val
default:
return d.Errf("unknown subdirective '%s'", d.Val())
}
}
case "precompressed":
- var order []string
- for d.NextArg() {
- modID := "http.precompressed." + d.Val()
+ fsrv.PrecompressedOrder = d.RemainingArgs()
+ if len(fsrv.PrecompressedOrder) == 0 {
+ fsrv.PrecompressedOrder = []string{"br", "zstd", "gzip"}
+ }
+
+ for _, format := range fsrv.PrecompressedOrder {
+ modID := "http.precompressed." + format
mod, err := caddy.GetModule(modID)
if err != nil {
return d.Errf("getting module named '%s': %v", modID, err)
@@ -150,10 +165,8 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if fsrv.PrecompressedRaw == nil {
fsrv.PrecompressedRaw = make(caddy.ModuleMap)
}
- fsrv.PrecompressedRaw[d.Val()] = caddyconfig.JSON(precompress, nil)
- order = append(order, d.Val())
+ fsrv.PrecompressedRaw[format] = caddyconfig.JSON(precompress, nil)
}
- fsrv.PrecompressedOrder = order
case "status":
if !d.NextArg() {
@@ -263,7 +276,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
tryPolicy = h.Val()
switch tryPolicy {
- case tryPolicyFirstExist, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
+ case tryPolicyFirstExist, tryPolicyFirstExistFallback, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
default:
return nil, h.Errf("unrecognized try policy: %s", tryPolicy)
}
diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go
index a76998405..a04d7cade 100644
--- a/modules/caddyhttp/fileserver/command.go
+++ b/modules/caddyhttp/fileserver/command.go
@@ -66,6 +66,7 @@ respond with a file listing.`,
cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log")
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
+ cmd.Flags().IntP("file-limit", "f", defaultDirEntryLimit, "Max directories to read")
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression")
cmd.Flags().StringSliceP("precompressed", "p", []string{}, "Specify precompression file extensions. Compression preference implied from flag order.")
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer)
@@ -91,6 +92,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
browse := fs.Bool("browse")
templates := fs.Bool("templates")
accessLog := fs.Bool("access-log")
+ fileLimit := fs.Int("file-limit")
debug := fs.Bool("debug")
revealSymlinks := fs.Bool("reveal-symlinks")
compress := !fs.Bool("no-compress")
@@ -151,7 +153,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
}
if browse {
- handler.Browse = &Browse{RevealSymlinks: revealSymlinks}
+ handler.Browse = &Browse{RevealSymlinks: revealSymlinks, FileLimit: fileLimit}
}
handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "file_server", nil))
diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go
index 71de1db29..2bc665d4f 100644
--- a/modules/caddyhttp/fileserver/matcher.go
+++ b/modules/caddyhttp/fileserver/matcher.go
@@ -90,6 +90,7 @@ type MatchFile struct {
// How to choose a file in TryFiles. Can be:
//
// - first_exist
+ // - first_exist_fallback
// - smallest_size
// - largest_size
// - most_recently_modified
@@ -173,7 +174,7 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
requestType := cel.ObjectType("http.Request")
- matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) {
+ matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcherWithError, error) {
values, err := caddyhttp.CELValueToMapStrList(data)
if err != nil {
return nil, err
@@ -191,7 +192,7 @@ func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
var try_policy string
if len(values["try_policy"]) > 0 {
- root = values["try_policy"][0]
+ try_policy = values["try_policy"][0]
}
m := MatchFile{
@@ -296,6 +297,7 @@ func (m MatchFile) Validate() error {
switch m.TryPolicy {
case "",
tryPolicyFirstExist,
+ tryPolicyFirstExistFallback,
tryPolicyLargestSize,
tryPolicySmallestSize,
tryPolicyMostRecentlyMod:
@@ -313,12 +315,22 @@ func (m MatchFile) Validate() error {
// - http.matchers.file.type: file or directory
// - http.matchers.file.remainder: Portion remaining after splitting file path (if configured)
func (m MatchFile) Match(r *http.Request) bool {
+ match, err := m.selectFile(r)
+ if err != nil {
+ // nolint:staticcheck
+ caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
+ }
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchFile) MatchWithError(r *http.Request) (bool, error) {
return m.selectFile(r)
}
// selectFile chooses a file according to m.TryPolicy by appending
// the paths in m.TryFiles to m.Root, with placeholder replacements.
-func (m MatchFile) selectFile(r *http.Request) (matched bool) {
+func (m MatchFile) selectFile(r *http.Request) (bool, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
root := filepath.Clean(repl.ReplaceAll(m.Root, "."))
@@ -330,7 +342,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
if c := m.logger.Check(zapcore.ErrorLevel, "use of unregistered filesystem"); c != nil {
c.Write(zap.String("fs", fsName))
}
- return false
+ return false, nil
}
type matchCandidate struct {
fullpath, relative, splitRemainder string
@@ -405,13 +417,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
// 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.absolute", filepath.ToSlash(candidate.fullpath))
repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder))
fileType := "file"
- if info.IsDir() {
+ if isDir {
fileType = "directory"
}
repl.Set("http.matchers.file.type", fileType)
@@ -419,17 +431,32 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
// match file according to the configured policy
switch m.TryPolicy {
- case "", tryPolicyFirstExist:
- for _, pattern := range m.TryFiles {
+ case "", tryPolicyFirstExist, tryPolicyFirstExistFallback:
+ 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,
+ // which short-circuits the middleware pipeline and
+ // writes an HTTP error response.
if err := parseErrorCode(pattern); err != nil {
- caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
- return
+ return false, err
}
+
candidates := makeCandidates(pattern)
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 {
- setPlaceholders(c, info)
- return true
+ setPlaceholders(c, info.IsDir())
+ return true, nil
}
}
}
@@ -450,10 +477,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
}
if largestInfo == nil {
- return false
+ return false, nil
}
- setPlaceholders(largest, largestInfo)
- return true
+ setPlaceholders(largest, largestInfo.IsDir())
+ return true, nil
case tryPolicySmallestSize:
var smallestSize int64
@@ -471,10 +498,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
}
if smallestInfo == nil {
- return false
+ return false, nil
}
- setPlaceholders(smallest, smallestInfo)
- return true
+ setPlaceholders(smallest, smallestInfo.IsDir())
+ return true, nil
case tryPolicyMostRecentlyMod:
var recent matchCandidate
@@ -491,13 +518,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
}
if recentInfo == nil {
- return false
+ return false, nil
}
- setPlaceholders(recent, recentInfo)
- return true
+ setPlaceholders(recent, recentInfo.IsDir())
+ return true, nil
}
- return
+ return false, nil
}
// parseErrorCode checks if the input is a status
@@ -695,15 +722,16 @@ var globSafeRepl = strings.NewReplacer(
)
const (
- tryPolicyFirstExist = "first_exist"
- tryPolicyLargestSize = "largest_size"
- tryPolicySmallestSize = "smallest_size"
- tryPolicyMostRecentlyMod = "most_recently_modified"
+ tryPolicyFirstExist = "first_exist"
+ tryPolicyFirstExistFallback = "first_exist_fallback"
+ tryPolicyLargestSize = "largest_size"
+ tryPolicySmallestSize = "smallest_size"
+ tryPolicyMostRecentlyMod = "most_recently_modified"
)
// Interface guards
var (
- _ caddy.Validator = (*MatchFile)(nil)
- _ caddyhttp.RequestMatcher = (*MatchFile)(nil)
- _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
+ _ caddy.Validator = (*MatchFile)(nil)
+ _ caddyhttp.RequestMatcherWithError = (*MatchFile)(nil)
+ _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
)
diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go
index 527c16bd1..b6697b9d8 100644
--- a/modules/caddyhttp/fileserver/matcher_test.go
+++ b/modules/caddyhttp/fileserver/matcher_test.go
@@ -130,7 +130,10 @@ func TestFileMatcher(t *testing.T) {
req := &http.Request{URL: u}
repl := caddyhttp.NewTestReplacer(req)
- result := m.Match(req)
+ result, err := m.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d: unexpected error: %v", i, err)
+ }
if result != tc.matched {
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
}
@@ -240,7 +243,10 @@ func TestPHPFileMatcher(t *testing.T) {
req := &http.Request{URL: u}
repl := caddyhttp.NewTestReplacer(req)
- result := m.Match(req)
+ result, err := m.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d: unexpected error: %v", i, err)
+ }
if result != tc.matched {
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
}
@@ -289,6 +295,7 @@ var expressionTests = []struct {
wantErr bool
wantResult bool
clientCertificate []byte
+ expectedPath string
}{
{
name: "file error no args (MatchFile)",
@@ -354,6 +361,15 @@ var expressionTests = []struct {
urlTarget: "https://example.com/nopenope.txt",
wantResult: false,
},
+ {
+ name: "file match long pattern foo.txt with try_policy (MatchFile)",
+ expression: &caddyhttp.MatchExpression{
+ Expr: `file({"root": "./testdata", "try_policy": "largest_size", "try_files": ["foo.txt", "large.txt"]})`,
+ },
+ urlTarget: "https://example.com/",
+ wantResult: true,
+ expectedPath: "/large.txt",
+ },
}
func TestMatchExpressionMatch(t *testing.T) {
@@ -379,9 +395,24 @@ func TestMatchExpressionMatch(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
- if tc.expression.Match(req) != tc.wantResult {
+ matches, err := tc.expression.MatchWithError(req)
+ if err != nil {
+ t.Errorf("MatchExpression.Match() error = %v", err)
+ return
+ }
+ if matches != tc.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
}
+
+ if tc.expectedPath != "" {
+ path, ok := repl.Get("http.matchers.file.relative")
+ if !ok {
+ t.Errorf("MatchExpression.Match() expected to return path '%s', but got none", tc.expectedPath)
+ }
+ if path != tc.expectedPath {
+ t.Errorf("MatchExpression.Match() expected to return path '%s', but got '%s'", tc.expectedPath, path)
+ }
+ }
})
}
}
diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go
index 4ae69b647..2b0caecfc 100644
--- a/modules/caddyhttp/fileserver/staticfiles.go
+++ b/modules/caddyhttp/fileserver/staticfiles.go
@@ -204,7 +204,7 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
// absolute paths before the server starts for very slight performance improvement
for i, h := range fsrv.Hide {
if !strings.Contains(h, "{") && strings.Contains(h, separator) {
- if abs, err := filepath.Abs(h); err == nil {
+ if abs, err := caddy.FastAbs(h); err == nil {
fsrv.Hide[i] = abs
}
}
@@ -636,7 +636,7 @@ func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string {
for i := range fsrv.Hide {
hide[i] = repl.ReplaceAll(fsrv.Hide[i], "")
if strings.Contains(hide[i], separator) {
- abs, err := filepath.Abs(hide[i])
+ abs, err := caddy.FastAbs(hide[i])
if err == nil {
hide[i] = abs
}
@@ -655,7 +655,7 @@ func fileHidden(filename string, hide []string) bool {
}
// all path comparisons use the complete absolute path if possible
- filenameAbs, err := filepath.Abs(filename)
+ filenameAbs, err := caddy.FastAbs(filename)
if err == nil {
filename = filenameAbs
}
diff --git a/modules/caddyhttp/fileserver/testdata/large.txt b/modules/caddyhttp/fileserver/testdata/large.txt
new file mode 100644
index 000000000..c36623744
--- /dev/null
+++ b/modules/caddyhttp/fileserver/testdata/large.txt
@@ -0,0 +1,3 @@
+This is a file with more content than the other files in this directory
+such that tests using the largest_size policy pick this file, or the
+smallest_size policy avoids this file. \ No newline at end of file