diff options
Diffstat (limited to 'modules/caddyhttp/fileserver')
-rw-r--r-- | modules/caddyhttp/fileserver/browse.go | 13 | ||||
-rw-r--r-- | modules/caddyhttp/fileserver/caddyfile.go | 29 | ||||
-rw-r--r-- | modules/caddyhttp/fileserver/command.go | 4 | ||||
-rw-r--r-- | modules/caddyhttp/fileserver/matcher.go | 86 | ||||
-rw-r--r-- | modules/caddyhttp/fileserver/matcher_test.go | 37 | ||||
-rw-r--r-- | modules/caddyhttp/fileserver/staticfiles.go | 6 | ||||
-rw-r--r-- | modules/caddyhttp/fileserver/testdata/large.txt | 3 |
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 |