@marcel91200 / caddy Mirror · read-only / commits / c839a98f
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 12, 18:15 UTC

filesystem: Globally declared filesystems, `fs` directive (#5833)

a committed Jan 13, 2024 at 14:12 UTC c839a98ff527932fd14460829142c486f4531a7b
30 files changed +450 -219
caddy.go
+7
@@ -39,6 +39,7 @@ import (
39 "github.com/google/uuid"
40 "go.uber.org/zap"
41
42 + "github.com/caddyserver/caddy/v2/internal/filesystems"
43 "github.com/caddyserver/caddy/v2/notify"
44 )
45
@@ -84,6 +85,9 @@ type Config struct {
85 storage certmagic.Storage
86
87 cancelFunc context.CancelFunc
88 +
89 + // filesystems is a dict of filesystems that will later be loaded from and added to.
90 + filesystems FileSystems
91 }
92
93 // App is a thing that Caddy runs.
@@ -447,6 +451,9 @@ func run(newCfg *Config, start bool) (Context, error) {
451 }
452 }
453
454 + // create the new filesystem map
455 + newCfg.filesystems = &filesystems.FilesystemMap{}
456 +
457 // prepare the new config for use
458 newCfg.apps = make(map[string]App)
459
caddyconfig/caddyfile/dispenser_test.go
+1 -1
@@ -305,7 +305,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
305 t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
306 }
307
308 - var ErrBarIsFull = errors.New("bar is full")
308 + ErrBarIsFull := errors.New("bar is full")
309 bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
310 if !errors.Is(bookingError, ErrBarIsFull) {
311 t.Errorf("Errf(): should be able to unwrap the error chain")
caddyconfig/caddyfile/parse_test.go
+9 -9
@@ -22,7 +22,7 @@ import (
22 )
23
24 func TestParseVariadic(t *testing.T) {
25 - var args = make([]string, 10)
25 + args := make([]string, 10)
26 for i, tc := range []struct {
27 input string
28 result bool
@@ -111,7 +111,6 @@ func TestAllTokens(t *testing.T) {
111 input := []byte("a b c\nd e")
112 expected := []string{"a", "b", "c", "d", "e"}
113 tokens, err := allTokens("TestAllTokens", input)
114 -
114 if err != nil {
115 t.Fatalf("Expected no error, got %v", err)
116 }
@@ -149,10 +148,11 @@ func TestParseOneAndImport(t *testing.T) {
148 "localhost",
149 }, []int{1}},
150
152 - {`localhost:1234
151 + {
152 + `localhost:1234
153 dir1 foo bar`, false, []string{
154 - "localhost:1234",
155 - }, []int{3},
154 + "localhost:1234",
155 + }, []int{3},
156 },
157
158 {`localhost {
@@ -407,13 +407,13 @@ func TestRecursiveImport(t *testing.T) {
407 err = os.WriteFile(recursiveFile1, []byte(
408 `localhost
409 dir1
410 - import recursive_import_test2`), 0644)
410 + import recursive_import_test2`), 0o644)
411 if err != nil {
412 t.Fatal(err)
413 }
414 defer os.Remove(recursiveFile1)
415
416 - err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
416 + err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0o644)
417 if err != nil {
418 t.Fatal(err)
419 }
@@ -441,7 +441,7 @@ func TestRecursiveImport(t *testing.T) {
441 err = os.WriteFile(recursiveFile1, []byte(
442 `localhost
443 dir1
444 - import `+recursiveFile2), 0644)
444 + import `+recursiveFile2), 0o644)
445 if err != nil {
446 t.Fatal(err)
447 }
@@ -495,7 +495,7 @@ func TestDirectiveImport(t *testing.T) {
495 }
496
497 err = os.WriteFile(directiveFile, []byte(`prop1 1
498 - prop2 2`), 0644)
498 + prop2 2`), 0o644)
499 if err != nil {
500 t.Fatal(err)
501 }
caddyconfig/httpcaddyfile/builtins.go
+18
@@ -40,6 +40,7 @@ import (
40 func init() {
41 RegisterDirective("bind", parseBind)
42 RegisterDirective("tls", parseTLS)
43 + RegisterHandlerDirective("fs", parseFilesystem)
44 RegisterHandlerDirective("root", parseRoot)
45 RegisterHandlerDirective("vars", parseVars)
46 RegisterHandlerDirective("redir", parseRedir)
@@ -658,6 +659,23 @@ func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
659 return caddyhttp.VarsMiddleware{"root": root}, nil
660 }
661
662 +// parseFilesystem parses the fs directive. Syntax:
663 +//
664 +// fs <filesystem>
665 +func parseFilesystem(h Helper) (caddyhttp.MiddlewareHandler, error) {
666 + var name string
667 + for h.Next() {
668 + if !h.NextArg() {
669 + return nil, h.ArgErr()
670 + }
671 + name = h.Val()
672 + if h.NextArg() {
673 + return nil, h.ArgErr()
674 + }
675 + }
676 + return caddyhttp.VarsMiddleware{"fs": name}, nil
677 +}
678 +
679 // parseVars parses the vars directive. See its UnmarshalCaddyfile method for syntax.
680 func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) {
681 v := new(caddyhttp.VarsMiddleware)
caddyconfig/httpcaddyfile/directives.go
+1
@@ -41,6 +41,7 @@ var directiveOrder = []string{
41
42 "map",
43 "vars",
44 + "fs",
45 "root",
46 "skip_log",
47
caddyconfig/httpcaddyfile/directives_test.go
+6 -3
@@ -31,20 +31,23 @@ func TestHostsFromKeys(t *testing.T) {
31 []Address{
32 {Original: ":2015", Port: "2015"},
33 },
34 - []string{}, []string{},
34 + []string{},
35 + []string{},
36 },
37 {
38 []Address{
39 {Original: ":443", Port: "443"},
40 },
40 - []string{}, []string{},
41 + []string{},
42 + []string{},
43 },
44 {
45 []Address{
46 {Original: "foo", Host: "foo"},
47 {Original: ":2015", Port: "2015"},
48 },
47 - []string{}, []string{"foo"},
49 + []string{},
50 + []string{"foo"},
51 },
52 {
53 []Address{
caddyconfig/httpcaddyfile/httptype.go
+6 -1
@@ -271,6 +271,12 @@ func (st ServerType) Setup(
271 if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) {
272 cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings)
273 }
274 + if filesystems, ok := options["filesystem"].(caddy.Module); ok {
275 + cfg.AppsRaw["caddy.filesystems"] = caddyconfig.JSON(
276 + filesystems,
277 + &warnings)
278 + }
279 +
280 if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
281 cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
282 "module",
@@ -280,7 +286,6 @@ func (st ServerType) Setup(
286 if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
287 cfg.Admin = adminConfig
288 }
283 -
289 if pc, ok := options["persist_config"].(string); ok && pc == "off" {
290 if cfg.Admin == nil {
291 cfg.Admin = new(caddy.AdminConfig)
caddytest/integration/caddyfile_test.go
-5
@@ -9,7 +9,6 @@ import (
9 )
10
11 func TestRespond(t *testing.T) {
12 -
12 // arrange
13 tester := caddytest.NewTester(t)
14 tester.InitServer(`
@@ -32,7 +31,6 @@ func TestRespond(t *testing.T) {
31 }
32
33 func TestRedirect(t *testing.T) {
35 -
34 // arrange
35 tester := caddytest.NewTester(t)
36 tester.InitServer(`
@@ -61,7 +59,6 @@ func TestRedirect(t *testing.T) {
59 }
60
61 func TestDuplicateHosts(t *testing.T) {
64 -
62 // act and assert
63 caddytest.AssertLoadError(t,
64 `
@@ -76,7 +73,6 @@ func TestDuplicateHosts(t *testing.T) {
73 }
74
75 func TestReadCookie(t *testing.T) {
79 -
76 localhost, _ := url.Parse("http://localhost")
77 cookie := http.Cookie{
78 Name: "clientname",
@@ -110,7 +106,6 @@ func TestReadCookie(t *testing.T) {
106 }
107
108 func TestReplIndex(t *testing.T) {
113 -
109 tester := caddytest.NewTester(t)
110 tester.InitServer(`
111 {
caddytest/integration/reverseproxy_test.go
-1
@@ -57,7 +57,6 @@ func TestSRVReverseProxy(t *testing.T) {
57 }
58
59 func TestDialWithPlaceholderUnix(t *testing.T) {
60 -
60 if runtime.GOOS == "windows" {
61 t.SkipNow()
62 }
caddytest/integration/sni_test.go
-2
@@ -7,7 +7,6 @@ import (
7 )
8
9 func TestDefaultSNI(t *testing.T) {
10 -
10 // arrange
11 tester := caddytest.NewTester(t)
12 tester.InitServer(`{
@@ -107,7 +106,6 @@ func TestDefaultSNI(t *testing.T) {
106 }
107
108 func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
110 -
109 // arrange
110 tester := caddytest.NewTester(t)
111 tester.InitServer(`
caddytest/integration/stream_test.go
-1
@@ -360,7 +360,6 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
360
361 func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
362 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
363 -
363 if r.Host != "127.0.0.1:9443" {
364 t.Errorf("r.Host doesn't match, %v!", r.Host)
365 w.WriteHeader(http.StatusNotFound)
context.go
+12
@@ -23,6 +23,8 @@ import (
23
24 "github.com/caddyserver/certmagic"
25 "go.uber.org/zap"
26 +
27 + "github.com/caddyserver/caddy/v2/internal/filesystems"
28 )
29
30 // Context is a type which defines the lifetime of modules that
@@ -37,6 +39,7 @@ import (
39 // not actually need to do this).
40 type Context struct {
41 context.Context
42 +
43 moduleInstances map[string][]Module
44 cfg *Config
45 cleanupFuncs []func()
@@ -81,6 +84,15 @@ func (ctx *Context) OnCancel(f func()) {
84 ctx.cleanupFuncs = append(ctx.cleanupFuncs, f)
85 }
86
87 +// Filesystems returns a ref to the FilesystemMap
88 +func (ctx *Context) Filesystems() FileSystems {
89 + // if no config is loaded, we use a default filesystemmap, which includes the osfs
90 + if ctx.cfg == nil {
91 + return &filesystems.FilesystemMap{}
92 + }
93 + return ctx.cfg.filesystems
94 +}
95 +
96 // LoadModule loads the Caddy module(s) from the specified field of the parent struct
97 // pointer and returns the loaded module(s). The struct pointer and its field name as
98 // a string are necessary so that reflection can be used to read the struct tag on the
filesystem.go new
+10
@@ -0,0 +1,10 @@
1 +package caddy
2 +
3 +import "io/fs"
4 +
5 +type FileSystems interface {
6 + Register(k string, v fs.FS)
7 + Unregister(k string)
8 + Get(k string) (v fs.FS, ok bool)
9 + Default() fs.FS
10 +}
internal/filesystems/map.go new
+77
@@ -0,0 +1,77 @@
1 +package filesystems
2 +
3 +import (
4 + "io/fs"
5 + "strings"
6 + "sync"
7 +)
8 +
9 +const (
10 + DefaultFilesystemKey = "default"
11 +)
12 +
13 +var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}}
14 +
15 +// wrapperFs exists so can easily add to wrapperFs down the line
16 +type wrapperFs struct {
17 + key string
18 + fs.FS
19 +}
20 +
21 +// FilesystemMap stores a map of filesystems
22 +// the empty key will be overwritten to be the default key
23 +// it includes a default filesystem, based off the os fs
24 +type FilesystemMap struct {
25 + m sync.Map
26 +}
27 +
28 +// note that the first invocation of key cannot be called in a racy context.
29 +func (f *FilesystemMap) key(k string) string {
30 + if k == "" {
31 + k = DefaultFilesystemKey
32 + }
33 + return k
34 +}
35 +
36 +// Register will add the filesystem with key to later be retrieved
37 +// A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil
38 +func (f *FilesystemMap) Register(k string, v fs.FS) {
39 + k = f.key(k)
40 + if v == nil {
41 + f.Unregister(k)
42 + return
43 + }
44 + f.m.Store(k, &wrapperFs{key: k, FS: v})
45 +}
46 +
47 +// Unregister will remove the filesystem with key from the filesystem map
48 +// if the key is the default key, it will set the default to the osFS instead of deleting it
49 +// modules should call this on cleanup to be safe
50 +func (f *FilesystemMap) Unregister(k string) {
51 + k = f.key(k)
52 + if k == DefaultFilesystemKey {
53 + f.m.Store(k, DefaultFilesystem)
54 + } else {
55 + f.m.Delete(k)
56 + }
57 +}
58 +
59 +// Get will get a filesystem with a given key
60 +func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
61 + k = f.key(k)
62 + c, ok := f.m.Load(strings.TrimSpace(k))
63 + if !ok {
64 + if k == DefaultFilesystemKey {
65 + f.m.Store(k, DefaultFilesystem)
66 + return DefaultFilesystem, true
67 + }
68 + return nil, ok
69 + }
70 + return c.(fs.FS), true
71 +}
72 +
73 +// Default will get the default filesystem in the filesystem map
74 +func (f *FilesystemMap) Default() fs.FS {
75 + val, _ := f.Get(DefaultFilesystemKey)
76 + return val
77 +}
internal/filesystems/os.go new
+29
@@ -0,0 +1,29 @@
1 +package filesystems
2 +
3 +import (
4 + "io/fs"
5 + "os"
6 + "path/filepath"
7 +)
8 +
9 +// OsFS is a simple fs.FS implementation that uses the local
10 +// file system. (We do not use os.DirFS because we do our own
11 +// rooting or path prefixing without being constrained to a single
12 +// root folder. The standard os.DirFS implementation is problematic
13 +// since roots can be dynamic in our application.)
14 +//
15 +// OsFS also implements fs.StatFS, fs.GlobFS, fs.ReadDirFS, and fs.ReadFileFS.
16 +type OsFS struct{}
17 +
18 +func (OsFS) Open(name string) (fs.File, error) { return os.Open(name) }
19 +func (OsFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
20 +func (OsFS) Glob(pattern string) ([]string, error) { return filepath.Glob(pattern) }
21 +func (OsFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(name) }
22 +func (OsFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) }
23 +
24 +var (
25 + _ fs.StatFS = (*OsFS)(nil)
26 + _ fs.GlobFS = (*OsFS)(nil)
27 + _ fs.ReadDirFS = (*OsFS)(nil)
28 + _ fs.ReadFileFS = (*OsFS)(nil)
29 +)
modules/caddyfs/filesystem.go new
+112
@@ -0,0 +1,112 @@
1 +package caddyfs
2 +
3 +import (
4 + "encoding/json"
5 + "fmt"
6 + "io/fs"
7 +
8 + "go.uber.org/zap"
9 +
10 + "github.com/caddyserver/caddy/v2"
11 + "github.com/caddyserver/caddy/v2/caddyconfig"
12 + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
13 + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
14 +)
15 +
16 +func init() {
17 + caddy.RegisterModule(Filesystems{})
18 + httpcaddyfile.RegisterGlobalOption("filesystem", parseFilesystems)
19 +}
20 +
21 +type moduleEntry struct {
22 + Key string `json:"name,omitempty"`
23 + FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"`
24 + fileSystem fs.FS
25 +}
26 +
27 +// Filesystems loads caddy.fs modules into the global filesystem map
28 +type Filesystems struct {
29 + Filesystems []*moduleEntry `json:"filesystems"`
30 +
31 + defers []func()
32 +}
33 +
34 +func parseFilesystems(d *caddyfile.Dispenser, existingVal any) (any, error) {
35 + p := &Filesystems{}
36 + current, ok := existingVal.(*Filesystems)
37 + if ok {
38 + p = current
39 + }
40 + x := &moduleEntry{}
41 + err := x.UnmarshalCaddyfile(d)
42 + if err != nil {
43 + return nil, err
44 + }
45 + p.Filesystems = append(p.Filesystems, x)
46 + return p, nil
47 +}
48 +
49 +// CaddyModule returns the Caddy module information.
50 +func (Filesystems) CaddyModule() caddy.ModuleInfo {
51 + return caddy.ModuleInfo{
52 + ID: "caddy.filesystems",
53 + New: func() caddy.Module { return new(Filesystems) },
54 + }
55 +}
56 +
57 +func (xs *Filesystems) Start() error { return nil }
58 +func (xs *Filesystems) Stop() error { return nil }
59 +
60 +func (xs *Filesystems) Provision(ctx caddy.Context) error {
61 + // load the filesystem module
62 + for _, f := range xs.Filesystems {
63 + if len(f.FileSystemRaw) > 0 {
64 + mod, err := ctx.LoadModule(f, "FileSystemRaw")
65 + if err != nil {
66 + return fmt.Errorf("loading file system module: %v", err)
67 + }
68 + f.fileSystem = mod.(fs.FS)
69 + }
70 + // register that module
71 + ctx.Logger().Debug("registering fs", zap.String("fs", f.Key))
72 + ctx.Filesystems().Register(f.Key, f.fileSystem)
73 + // remember to unregister the module when we are done
74 + xs.defers = append(xs.defers, func() {
75 + ctx.Logger().Debug("registering fs", zap.String("fs", f.Key))
76 + ctx.Filesystems().Unregister(f.Key)
77 + })
78 + }
79 + return nil
80 +}
81 +
82 +func (f *Filesystems) Cleanup() error {
83 + for _, v := range f.defers {
84 + v()
85 + }
86 + return nil
87 +}
88 +
89 +func (f *moduleEntry) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
90 + for d.Next() {
91 + // key required for now
92 + if !d.Args(&f.Key) {
93 + return d.ArgErr()
94 + }
95 + // get the module json
96 + if !d.NextArg() {
97 + return d.ArgErr()
98 + }
99 + name := d.Val()
100 + modID := "caddy.fs." + name
101 + unm, err := caddyfile.UnmarshalModule(d, modID)
102 + if err != nil {
103 + return err
104 + }
105 + fsys, ok := unm.(fs.FS)
106 + if !ok {
107 + return d.Errf("module %s (%T) is not a supported file system implementation (requires fs.FS)", modID, unm)
108 + }
109 + f.FileSystemRaw = caddyconfig.JSONModuleObject(fsys, "backend", name, nil)
110 + }
111 + return nil
112 +}
modules/caddyhttp/encode/encode_test.go
-2
@@ -105,7 +105,6 @@ func TestPreferOrder(t *testing.T) {
105
106 for _, test := range testCases {
107 t.Run(test.name, func(t *testing.T) {
108 -
108 if test.accept == "" {
109 r.Header.Del("Accept-Encoding")
110 } else {
@@ -258,7 +257,6 @@ func TestValidate(t *testing.T) {
257 t.Errorf("Validate() error = %v, wantErr = %v", err, test.wantErr)
258 }
259 })
261 -
260 }
261 }
262
modules/caddyhttp/fileserver/browse.go
+7 -7
@@ -52,7 +52,7 @@ type Browse struct {
52 TemplateFile string `json:"template_file,omitempty"`
53 }
54
55 -func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
55 +func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
56 fsrv.logger.Debug("browse enabled; listing directory contents",
57 zap.String("path", dirPath),
58 zap.String("root", root))
@@ -82,7 +82,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
82 }
83 }
84
85 - dir, err := fsrv.openFile(dirPath, w)
85 + dir, err := fsrv.openFile(fileSystem, dirPath, w)
86 if err != nil {
87 return err
88 }
@@ -91,7 +91,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
91 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
92
93 // TODO: not entirely sure if path.Clean() is necessary here but seems like a safe plan (i.e. /%2e%2e%2f) - someone could verify this
94 - listing, err := fsrv.loadDirectoryContents(r.Context(), dir.(fs.ReadDirFile), root, path.Clean(r.URL.EscapedPath()), repl)
94 + listing, err := fsrv.loadDirectoryContents(r.Context(), fileSystem, dir.(fs.ReadDirFile), root, path.Clean(r.URL.EscapedPath()), repl)
95 switch {
96 case errors.Is(err, fs.ErrPermission):
97 return caddyhttp.Error(http.StatusForbidden, err)
@@ -145,7 +145,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
145 return nil
146 }
147
148 -func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) {
148 +func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs.FS, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) {
149 files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
150 if err != nil && err != io.EOF {
151 return nil, err
@@ -154,7 +154,7 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDi
154 // user can presumably browse "up" to parent folder if path is longer than "/"
155 canGoUp := len(urlPath) > 1
156
157 - return fsrv.directoryListing(ctx, files, canGoUp, root, urlPath, repl), nil
157 + return fsrv.directoryListing(ctx, fileSystem, files, canGoUp, root, urlPath, repl), nil
158 }
159
160 // browseApplyQueryParams applies query parameters to the listing.
@@ -223,12 +223,12 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.T
223
224 // isSymlinkTargetDir returns true if f's symbolic link target
225 // is a directory.
226 -func (fsrv *FileServer) isSymlinkTargetDir(f fs.FileInfo, root, urlPath string) bool {
226 +func (fsrv *FileServer) isSymlinkTargetDir(fileSystem fs.FS, f fs.FileInfo, root, urlPath string) bool {
227 if !isSymlink(f) {
228 return false
229 }
230 target := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name()))
231 - targetInfo, err := fs.Stat(fsrv.fileSystem, target)
231 + targetInfo, err := fs.Stat(fileSystem, target)
232 if err != nil {
233 return false
234 }
modules/caddyhttp/fileserver/browsetplcontext.go
+3 -3
@@ -32,7 +32,7 @@ import (
32 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
33 )
34
35 -func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext {
35 +func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext {
36 filesToHide := fsrv.transformHidePaths(repl)
37
38 name, _ := url.PathUnescape(urlPath)
@@ -62,7 +62,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
62 continue
63 }
64
65 - isDir := entry.IsDir() || fsrv.isSymlinkTargetDir(info, root, urlPath)
65 + isDir := entry.IsDir() || fsrv.isSymlinkTargetDir(fileSystem, info, root, urlPath)
66
67 // add the slash after the escape of path to avoid escaping the slash as well
68 if isDir {
@@ -76,7 +76,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
76 fileIsSymlink := isSymlink(info)
77 if fileIsSymlink {
78 path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, info.Name()))
79 - fileInfo, err := fs.Stat(fsrv.fileSystem, path)
79 + fileInfo, err := fs.Stat(fileSystem, path)
80 if err == nil {
81 size = fileInfo.Size()
82 }
modules/caddyhttp/fileserver/caddyfile.go
+4 -17
@@ -15,13 +15,11 @@
15 package fileserver
16
17 import (
18 - "io/fs"
18 "path/filepath"
19 "strings"
20
21 "github.com/caddyserver/caddy/v2"
22 "github.com/caddyserver/caddy/v2/caddyconfig"
24 - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
23 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
24 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
25 "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
@@ -37,7 +35,7 @@ func init() {
35 // server and configures it with this syntax:
36 //
37 // file_server [<matcher>] [browse] {
40 -// fs <backend...>
38 +// fs <filesystem>
39 // root <path>
40 // hide <files...>
41 // index <files...>
@@ -68,21 +66,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
66 if !h.NextArg() {
67 return nil, h.ArgErr()
68 }
71 - if fsrv.FileSystemRaw != nil {
72 - return nil, h.Err("file system module already specified")
69 + if fsrv.FileSystem != "" {
70 + return nil, h.Err("file system already specified")
71 }
74 - name := h.Val()
75 - modID := "caddy.fs." + name
76 - unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
77 - if err != nil {
78 - return nil, err
79 - }
80 - fsys, ok := unm.(fs.FS)
81 - if !ok {
82 - return nil, h.Errf("module %s (%T) is not a supported file system implementation (requires fs.FS)", modID, unm)
83 - }
84 - fsrv.FileSystemRaw = caddyconfig.JSONModuleObject(fsys, "backend", name, nil)
85 -
72 + fsrv.FileSystem = h.Val()
73 case "hide":
74 fsrv.Hide = h.RemainingArgs()
75 if len(fsrv.Hide) == 0 {
modules/caddyhttp/fileserver/matcher.go
+32 -25
@@ -15,7 +15,6 @@
15 package fileserver
16
17 import (
18 - "encoding/json"
18 "fmt"
19 "io/fs"
20 "net/http"
@@ -64,8 +63,7 @@ func init() {
63 type MatchFile struct {
64 // The file system implementation to use. By default, the
65 // local disk file system will be used.
67 - FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"`
68 - fileSystem fs.FS
66 + FileSystem string `json:"fs,omitempty"`
67
68 // The root directory, used for creating absolute
69 // file paths, and required when working with
@@ -108,6 +106,8 @@ type MatchFile struct {
106 // component in order to be used as a split delimiter.
107 SplitPath []string `json:"split_path,omitempty"`
108
109 + fsmap caddy.FileSystems
110 +
111 logger *zap.Logger
112 }
113
@@ -181,16 +181,22 @@ func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
181 root = values["root"][0]
182 }
183
184 + var fsName string
185 + if len(values["fs"]) > 0 {
186 + fsName = values["fs"][0]
187 + }
188 +
189 var try_policy string
190 if len(values["try_policy"]) > 0 {
191 root = values["try_policy"][0]
192 }
193
194 m := MatchFile{
190 - Root: root,
191 - TryFiles: values["try_files"],
192 - TryPolicy: try_policy,
193 - SplitPath: values["split_path"],
195 + Root: root,
196 + TryFiles: values["try_files"],
197 + TryPolicy: try_policy,
198 + SplitPath: values["split_path"],
199 + FileSystem: fsName,
200 }
201
202 err = m.Provision(ctx)
@@ -264,22 +270,16 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
270 func (m *MatchFile) Provision(ctx caddy.Context) error {
271 m.logger = ctx.Logger()
272
267 - // establish the file system to use
268 - if len(m.FileSystemRaw) > 0 {
269 - mod, err := ctx.LoadModule(m, "FileSystemRaw")
270 - if err != nil {
271 - return fmt.Errorf("loading file system module: %v", err)
272 - }
273 - m.fileSystem = mod.(fs.FS)
274 - }
275 - if m.fileSystem == nil {
276 - m.fileSystem = osFS{}
277 - }
273 + m.fsmap = ctx.Filesystems()
274
275 if m.Root == "" {
276 m.Root = "{http.vars.root}"
277 }
278
279 + if m.FileSystem == "" {
280 + m.FileSystem = "{http.vars.fs}"
281 + }
282 +
283 // if list of files to try was omitted entirely, assume URL path
284 // (use placeholder instead of r.URL.Path; see issue #4146)
285 if m.TryFiles == nil {
@@ -320,6 +320,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
320
321 root := filepath.Clean(repl.ReplaceAll(m.Root, "."))
322
323 + fsName := repl.ReplaceAll(m.FileSystem, "")
324 +
325 + fileSystem, ok := m.fsmap.Get(fsName)
326 + if !ok {
327 + m.logger.Error("use of unregistered filesystem", zap.String("fs", fsName))
328 + return false
329 + }
330 type matchCandidate struct {
331 fullpath, relative, splitRemainder string
332 }
@@ -368,7 +375,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
375 if runtime.GOOS == "windows" {
376 globResults = []string{fullPattern} // precious Windows
377 } else {
371 - globResults, err = fs.Glob(m.fileSystem, fullPattern)
378 + globResults, err = fs.Glob(fileSystem, fullPattern)
379 if err != nil {
380 m.logger.Error("expanding glob", zap.Error(err))
381 }
@@ -410,7 +417,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
417 }
418 candidates := makeCandidates(pattern)
419 for _, c := range candidates {
413 - if info, exists := m.strictFileExists(c.fullpath); exists {
420 + if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists {
421 setPlaceholders(c, info)
422 return true
423 }
@@ -424,7 +431,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
431 for _, pattern := range m.TryFiles {
432 candidates := makeCandidates(pattern)
433 for _, c := range candidates {
427 - info, err := fs.Stat(m.fileSystem, c.fullpath)
434 + info, err := fs.Stat(fileSystem, c.fullpath)
435 if err == nil && info.Size() > largestSize {
436 largestSize = info.Size()
437 largest = c
@@ -445,7 +452,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
452 for _, pattern := range m.TryFiles {
453 candidates := makeCandidates(pattern)
454 for _, c := range candidates {
448 - info, err := fs.Stat(m.fileSystem, c.fullpath)
455 + info, err := fs.Stat(fileSystem, c.fullpath)
456 if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
457 smallestSize = info.Size()
458 smallest = c
@@ -465,7 +472,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
472 for _, pattern := range m.TryFiles {
473 candidates := makeCandidates(pattern)
474 for _, c := range candidates {
468 - info, err := fs.Stat(m.fileSystem, c.fullpath)
475 + info, err := fs.Stat(fileSystem, c.fullpath)
476 if err == nil &&
477 (recentInfo == nil || info.ModTime().After(recentInfo.ModTime())) {
478 recent = c
@@ -503,8 +510,8 @@ func parseErrorCode(input string) error {
510 // the file must also be a directory; if it does
511 // NOT end in a forward slash, the file must NOT
512 // be a directory.
506 -func (m MatchFile) strictFileExists(file string) (os.FileInfo, bool) {
507 - info, err := fs.Stat(m.fileSystem, file)
513 +func (m MatchFile) strictFileExists(fileSystem fs.FS, file string) (os.FileInfo, bool) {
514 + info, err := fs.Stat(fileSystem, file)
515 if err != nil {
516 // in reality, this can be any error
517 // such as permission or even obscure
modules/caddyhttp/fileserver/matcher_test.go
+79 -77
@@ -24,6 +24,7 @@ import (
24 "testing"
25
26 "github.com/caddyserver/caddy/v2"
27 + "github.com/caddyserver/caddy/v2/internal/filesystems"
28 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
29 )
30
@@ -116,9 +117,9 @@ func TestFileMatcher(t *testing.T) {
117 },
118 } {
119 m := &MatchFile{
119 - fileSystem: osFS{},
120 - Root: "./testdata",
121 - TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
120 + fsmap: &filesystems.FilesystemMap{},
121 + Root: "./testdata",
122 + TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
123 }
124
125 u, err := url.Parse(tc.path)
@@ -225,10 +226,10 @@ func TestPHPFileMatcher(t *testing.T) {
226 },
227 } {
228 m := &MatchFile{
228 - fileSystem: osFS{},
229 - Root: "./testdata",
230 - TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"},
231 - SplitPath: []string{".php"},
229 + fsmap: &filesystems.FilesystemMap{},
230 + Root: "./testdata",
231 + TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"},
232 + SplitPath: []string{".php"},
233 }
234
235 u, err := url.Parse(tc.path)
@@ -264,7 +265,10 @@ func TestPHPFileMatcher(t *testing.T) {
265 }
266
267 func TestFirstSplit(t *testing.T) {
267 - m := MatchFile{SplitPath: []string{".php"}}
268 + m := MatchFile{
269 + SplitPath: []string{".php"},
270 + fsmap: &filesystems.FilesystemMap{},
271 + }
272 actual, remainder := m.firstSplit("index.PHP/somewhere")
273 expected := "index.PHP"
274 expectedRemainder := "/somewhere"
@@ -276,83 +280,81 @@ func TestFirstSplit(t *testing.T) {
280 }
281 }
282
279 -var (
280 - expressionTests = []struct {
281 - name string
282 - expression *caddyhttp.MatchExpression
283 - urlTarget string
284 - httpMethod string
285 - httpHeader *http.Header
286 - wantErr bool
287 - wantResult bool
288 - clientCertificate []byte
289 - }{
290 - {
291 - name: "file error no args (MatchFile)",
292 - expression: &caddyhttp.MatchExpression{
293 - Expr: `file()`,
294 - },
295 - urlTarget: "https://example.com/foo.txt",
296 - wantResult: true,
283 +var expressionTests = []struct {
284 + name string
285 + expression *caddyhttp.MatchExpression
286 + urlTarget string
287 + httpMethod string
288 + httpHeader *http.Header
289 + wantErr bool
290 + wantResult bool
291 + clientCertificate []byte
292 +}{
293 + {
294 + name: "file error no args (MatchFile)",
295 + expression: &caddyhttp.MatchExpression{
296 + Expr: `file()`,
297 },
298 - {
299 - name: "file error bad try files (MatchFile)",
300 - expression: &caddyhttp.MatchExpression{
301 - Expr: `file({"try_file": ["bad_arg"]})`,
302 - },
303 - urlTarget: "https://example.com/foo",
304 - wantErr: true,
298 + urlTarget: "https://example.com/foo.txt",
299 + wantResult: true,
300 + },
301 + {
302 + name: "file error bad try files (MatchFile)",
303 + expression: &caddyhttp.MatchExpression{
304 + Expr: `file({"try_file": ["bad_arg"]})`,
305 },
306 - {
307 - name: "file match short pattern index.php (MatchFile)",
308 - expression: &caddyhttp.MatchExpression{
309 - Expr: `file("index.php")`,
310 - },
311 - urlTarget: "https://example.com/foo",
312 - wantResult: true,
306 + urlTarget: "https://example.com/foo",
307 + wantErr: true,
308 + },
309 + {
310 + name: "file match short pattern index.php (MatchFile)",
311 + expression: &caddyhttp.MatchExpression{
312 + Expr: `file("index.php")`,
313 },
314 - {
315 - name: "file match short pattern foo.txt (MatchFile)",
316 - expression: &caddyhttp.MatchExpression{
317 - Expr: `file({http.request.uri.path})`,
318 - },
319 - urlTarget: "https://example.com/foo.txt",
320 - wantResult: true,
314 + urlTarget: "https://example.com/foo",
315 + wantResult: true,
316 + },
317 + {
318 + name: "file match short pattern foo.txt (MatchFile)",
319 + expression: &caddyhttp.MatchExpression{
320 + Expr: `file({http.request.uri.path})`,
321 },
322 - {
323 - name: "file match index.php (MatchFile)",
324 - expression: &caddyhttp.MatchExpression{
325 - Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`,
326 - },
327 - urlTarget: "https://example.com/foo",
328 - wantResult: true,
322 + urlTarget: "https://example.com/foo.txt",
323 + wantResult: true,
324 + },
325 + {
326 + name: "file match index.php (MatchFile)",
327 + expression: &caddyhttp.MatchExpression{
328 + Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`,
329 },
330 - {
331 - name: "file match long pattern foo.txt (MatchFile)",
332 - expression: &caddyhttp.MatchExpression{
333 - Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
334 - },
335 - urlTarget: "https://example.com/foo.txt",
336 - wantResult: true,
330 + urlTarget: "https://example.com/foo",
331 + wantResult: true,
332 + },
333 + {
334 + name: "file match long pattern foo.txt (MatchFile)",
335 + expression: &caddyhttp.MatchExpression{
336 + Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
337 },
338 - {
339 - name: "file match long pattern foo.txt with concatenation (MatchFile)",
340 - expression: &caddyhttp.MatchExpression{
341 - Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`,
342 - },
343 - urlTarget: "https://example.com/foo.txt",
344 - wantResult: true,
338 + urlTarget: "https://example.com/foo.txt",
339 + wantResult: true,
340 + },
341 + {
342 + name: "file match long pattern foo.txt with concatenation (MatchFile)",
343 + expression: &caddyhttp.MatchExpression{
344 + Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`,
345 },
346 - {
347 - name: "file not match long pattern (MatchFile)",
348 - expression: &caddyhttp.MatchExpression{
349 - Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
350 - },
351 - urlTarget: "https://example.com/nopenope.txt",
352 - wantResult: false,
346 + urlTarget: "https://example.com/foo.txt",
347 + wantResult: true,
348 + },
349 + {
350 + name: "file not match long pattern (MatchFile)",
351 + expression: &caddyhttp.MatchExpression{
352 + Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
353 },
354 - }
355 -)
354 + urlTarget: "https://example.com/nopenope.txt",
355 + wantResult: false,
356 + },
357 +}
358
359 func TestMatchExpressionMatch(t *testing.T) {
360 for _, tst := range expressionTests {
modules/caddyhttp/fileserver/staticfiles.go
+27 -52
@@ -15,7 +15,6 @@
15 package fileserver
16
17 import (
18 - "encoding/json"
18 "errors"
19 "fmt"
20 "io"
@@ -97,15 +96,8 @@ type FileServer struct {
96 // The file system implementation to use. By default, Caddy uses the local
97 // disk file system.
98 //
100 - // File system modules used here must adhere to the following requirements:
101 - // - Implement fs.FS interface.
102 - // - Support seeking on opened files; i.e.returned fs.File values must
103 - // implement the io.Seeker interface. This is required for determining
104 - // Content-Length and satisfying Range requests.
105 - // - fs.File values that represent directories must implement the
106 - // fs.ReadDirFile interface so that directory listings can be procured.
107 - FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"`
108 - fileSystem fs.FS
99 + // if a non default filesystem is used, it must be first be registered in the globals section.
100 + FileSystem string `json:"fs,omitempty"`
101
102 // The path to the root of the site. Default is `{http.vars.root}` if set,
103 // or current working directory otherwise. This should be a trusted value.
@@ -169,6 +161,8 @@ type FileServer struct {
161 PrecompressedOrder []string `json:"precompressed_order,omitempty"`
162 precompressors map[string]encode.Precompressed
163
164 + fsmap caddy.FileSystems
165 +
166 logger *zap.Logger
167 }
168
@@ -184,16 +178,10 @@ func (FileServer) CaddyModule() caddy.ModuleInfo {
178 func (fsrv *FileServer) Provision(ctx caddy.Context) error {
179 fsrv.logger = ctx.Logger()
180
187 - // establish which file system (possibly a virtual one) we'll be using
188 - if len(fsrv.FileSystemRaw) > 0 {
189 - mod, err := ctx.LoadModule(fsrv, "FileSystemRaw")
190 - if err != nil {
191 - return fmt.Errorf("loading file system module: %v", err)
192 - }
193 - fsrv.fileSystem = mod.(fs.FS)
194 - }
195 - if fsrv.fileSystem == nil {
196 - fsrv.fileSystem = osFS{}
181 + fsrv.fsmap = ctx.Filesystems()
182 +
183 + if fsrv.FileSystem == "" {
184 + fsrv.FileSystem = "{http.vars.fs}"
185 }
186
187 if fsrv.Root == "" {
@@ -263,19 +251,26 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
251 filesToHide := fsrv.transformHidePaths(repl)
252
253 root := repl.ReplaceAll(fsrv.Root, ".")
254 + fsName := repl.ReplaceAll(fsrv.FileSystem, "")
255 +
256 + fileSystem, ok := fsrv.fsmap.Get(fsName)
257 + if !ok {
258 + return caddyhttp.Error(http.StatusNotFound, fmt.Errorf("filesystem not found"))
259 + }
260
261 // remove any trailing `/` as it breaks fs.ValidPath() in the stdlib
262 filename := strings.TrimSuffix(caddyhttp.SanitizedPathJoin(root, r.URL.Path), "/")
263
264 fsrv.logger.Debug("sanitized path join",
265 zap.String("site_root", root),
266 + zap.String("fs", fsName),
267 zap.String("request_path", r.URL.Path),
268 zap.String("result", filename))
269
270 // get information about the file
276 - info, err := fs.Stat(fsrv.fileSystem, filename)
271 + info, err := fs.Stat(fileSystem, filename)
272 if err != nil {
278 - err = fsrv.mapDirOpenError(err, filename)
273 + err = fsrv.mapDirOpenError(fileSystem, err, filename)
274 if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) {
275 return fsrv.notFound(w, r, next)
276 } else if errors.Is(err, fs.ErrPermission) {
@@ -299,7 +294,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
294 continue
295 }
296
302 - indexInfo, err := fs.Stat(fsrv.fileSystem, indexPath)
297 + indexInfo, err := fs.Stat(fileSystem, indexPath)
298 if err != nil {
299 continue
300 }
@@ -327,7 +322,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
322 zap.String("path", filename),
323 zap.Strings("index_filenames", fsrv.IndexNames))
324 if fsrv.Browse != nil && !fileHidden(filename, filesToHide) {
330 - return fsrv.serveBrowse(root, filename, w, r, next)
325 + return fsrv.serveBrowse(fileSystem, root, filename, w, r, next)
326 }
327 return fsrv.notFound(w, r, next)
328 }
@@ -381,13 +376,13 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
376 continue
377 }
378 compressedFilename := filename + precompress.Suffix()
384 - compressedInfo, err := fs.Stat(fsrv.fileSystem, compressedFilename)
379 + compressedInfo, err := fs.Stat(fileSystem, compressedFilename)
380 if err != nil || compressedInfo.IsDir() {
381 fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err))
382 continue
383 }
384 fsrv.logger.Debug("opening compressed sidecar file", zap.String("filename", compressedFilename), zap.Error(err))
390 - file, err = fsrv.openFile(compressedFilename, w)
385 + file, err = fsrv.openFile(fileSystem, compressedFilename, w)
386 if err != nil {
387 fsrv.logger.Warn("opening precompressed file failed", zap.String("filename", compressedFilename), zap.Error(err))
388 if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable {
@@ -416,7 +411,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
411 fsrv.logger.Debug("opening file", zap.String("filename", filename))
412
413 // open the file
419 - file, err = fsrv.openFile(filename, w)
414 + file, err = fsrv.openFile(fileSystem, filename, w)
415 if err != nil {
416 if herr, ok := err.(caddyhttp.HandlerError); ok &&
417 herr.StatusCode == http.StatusNotFound {
@@ -502,10 +497,10 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
497 // the response is configured to inform the client how to best handle it
498 // and a well-described handler error is returned (do not wrap the
499 // returned error value).
505 -func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (fs.File, error) {
506 - file, err := fsrv.fileSystem.Open(filename)
500 +func (fsrv *FileServer) openFile(fileSystem fs.FS, filename string, w http.ResponseWriter) (fs.File, error) {
501 + file, err := fileSystem.Open(filename)
502 if err != nil {
508 - err = fsrv.mapDirOpenError(err, filename)
503 + err = fsrv.mapDirOpenError(fileSystem, err, filename)
504 if errors.Is(err, fs.ErrNotExist) {
505 fsrv.logger.Debug("file not found", zap.String("filename", filename), zap.Error(err))
506 return nil, caddyhttp.Error(http.StatusNotFound, err)
@@ -530,7 +525,7 @@ func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (fs.Fil
525 // Adapted from the Go standard library; originally written by Nathaniel Caza.
526 // https://go-review.googlesource.com/c/go/+/36635/
527 // https://go-review.googlesource.com/c/go/+/36804/
533 -func (fsrv *FileServer) mapDirOpenError(originalErr error, name string) error {
528 +func (fsrv *FileServer) mapDirOpenError(fileSystem fs.FS, originalErr error, name string) error {
529 if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) {
530 return originalErr
531 }
@@ -540,7 +535,7 @@ func (fsrv *FileServer) mapDirOpenError(originalErr error, name string) error {
535 if parts[i] == "" {
536 continue
537 }
543 - fi, err := fs.Stat(fsrv.fileSystem, strings.Join(parts[:i+1], separator))
538 + fi, err := fs.Stat(fileSystem, strings.Join(parts[:i+1], separator))
539 if err != nil {
540 return originalErr
541 }
@@ -673,21 +668,6 @@ func (wr statusOverrideResponseWriter) Unwrap() http.ResponseWriter {
668 return wr.ResponseWriter
669 }
670
676 -// osFS is a simple fs.FS implementation that uses the local
677 -// file system. (We do not use os.DirFS because we do our own
678 -// rooting or path prefixing without being constrained to a single
679 -// root folder. The standard os.DirFS implementation is problematic
680 -// since roots can be dynamic in our application.)
681 -//
682 -// osFS also implements fs.StatFS, fs.GlobFS, fs.ReadDirFS, and fs.ReadFileFS.
683 -type osFS struct{}
684 -
685 -func (osFS) Open(name string) (fs.File, error) { return os.Open(name) }
686 -func (osFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
687 -func (osFS) Glob(pattern string) ([]string, error) { return filepath.Glob(pattern) }
688 -func (osFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(name) }
689 -func (osFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) }
690 -
671 var defaultIndexNames = []string{"index.html", "index.txt"}
672
673 const (
@@ -699,9 +679,4 @@ const (
679 var (
680 _ caddy.Provisioner = (*FileServer)(nil)
681 _ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
702 -
703 - _ fs.StatFS = (*osFS)(nil)
704 - _ fs.GlobFS = (*osFS)(nil)
705 - _ fs.ReadDirFS = (*osFS)(nil)
706 - _ fs.ReadFileFS = (*osFS)(nil)
682 )
modules/caddyhttp/matchers_test.go
+1 -1
@@ -862,7 +862,6 @@ func TestHeaderREMatcher(t *testing.T) {
862 }
863
864 func BenchmarkHeaderREMatcher(b *testing.B) {
865 -
865 i := 0
866 match := MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}}
867 input := http.Header{"Field": []string{"foobar"}}
@@ -1086,6 +1085,7 @@ func TestNotMatcher(t *testing.T) {
1085 }
1086 }
1087 }
1088 +
1089 func BenchmarkLargeHostMatcher(b *testing.B) {
1090 // this benchmark simulates a large host matcher (thousands of entries) where each
1091 // value is an exact hostname (not a placeholder or wildcard) - compare the results
modules/caddyhttp/reverseproxy/ascii_test.go
+2 -2
@@ -26,7 +26,7 @@ package reverseproxy
26 import "testing"
27
28 func TestEqualFold(t *testing.T) {
29 - var tests = []struct {
29 + tests := []struct {
30 name string
31 a, b string
32 want bool
@@ -64,7 +64,7 @@ func TestEqualFold(t *testing.T) {
64 }
65
66 func TestIsPrint(t *testing.T) {
67 - var tests = []struct {
67 + tests := []struct {
68 name string
69 in string
70 want bool
modules/caddyhttp/reverseproxy/fastcgi/client_test.go
+5 -7
@@ -48,7 +48,7 @@ import (
48 // and output "FAILED" in response
49 const (
50 scriptFile = "/tank/www/fcgic_test.php"
51 - //ipPort = "remote-php-serv:59000"
51 + // ipPort = "remote-php-serv:59000"
52 ipPort = "127.0.0.1:59000"
53 )
54
@@ -57,7 +57,6 @@ var globalt *testing.T
57 type FastCGIServer struct{}
58
59 func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
60 -
60 if err := req.ParseMultipartForm(100000000); err != nil {
61 log.Printf("[ERROR] failed to parse: %v", err)
62 }
@@ -84,7 +83,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
83 if req.MultipartForm != nil {
84 fileNum = len(req.MultipartForm.File)
85 for kn, fns := range req.MultipartForm.File {
87 - //fmt.Fprintln(resp, "server:filekey ", kn )
86 + // fmt.Fprintln(resp, "server:filekey ", kn )
87 length += len(kn)
88 for _, f := range fns {
89 fd, err := f.Open()
@@ -101,13 +100,13 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
100 length += int(l0)
101 defer fd.Close()
102 md5 := fmt.Sprintf("%x", h.Sum(nil))
104 - //fmt.Fprintln(resp, "server:filemd5 ", md5 )
103 + // fmt.Fprintln(resp, "server:filemd5 ", md5 )
104
105 if kn != md5 {
106 fmt.Fprintln(resp, "server:err ", md5, kn)
107 stat = "FAILED"
108 }
110 - //fmt.Fprintln(resp, "server:filename ", f.Filename )
109 + // fmt.Fprintln(resp, "server:filename ", f.Filename )
110 }
111 }
112 }
@@ -181,7 +180,6 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
180 }
181
182 func generateRandFile(size int) (p string, m string) {
184 -
183 p = filepath.Join(os.TempDir(), "fcgict"+strconv.Itoa(rand.Int()))
184
185 // open output file
@@ -236,7 +234,7 @@ func DisabledTest(t *testing.T) {
234 fcgiParams := make(map[string]string)
235 fcgiParams["REQUEST_METHOD"] = "GET"
236 fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1"
239 - //fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1"
237 + // fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1"
238 fcgiParams["SCRIPT_FILENAME"] = scriptFile
239
240 // simple GET
modules/caddyhttp/reverseproxy/selectionpolicies_test.go
-1
@@ -629,7 +629,6 @@ func TestRandomChoicePolicy(t *testing.T) {
629 if h == pool[0] {
630 t.Error("RandomChoicePolicy should not choose pool[0]")
631 }
632 -
632 }
633
634 func TestCookieHashPolicy(t *testing.T) {
modules/caddyhttp/rewrite/rewrite_test.go
+1 -1
@@ -333,7 +333,7 @@ func TestRewrite(t *testing.T) {
333 input: newRequest(t, "GET", "/foo/findme%2Fbar"),
334 expect: newRequest(t, "GET", "/foo/replaced%2Fbar"),
335 },
336 -
336 +
337 {
338 rule: Rewrite{PathRegexp: []*regexReplacer{{Find: "/{2,}", Replace: "/"}}},
339 input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"),
modules/caddyhttp/tracing/tracerprovider_test.go
-1
@@ -28,7 +28,6 @@ func Test_tracersProvider_cleanupTracerProvider(t *testing.T) {
28 tp.getTracerProvider()
29
30 err := tp.cleanupTracerProvider(zap.NewNop())
31 -
31 if err != nil {
32 t.Errorf("There should be no error: %v", err)
33 }
modules/standard/imports.go
+1
@@ -5,6 +5,7 @@ import (
5 _ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
6 _ "github.com/caddyserver/caddy/v2/modules/caddyevents"
7 _ "github.com/caddyserver/caddy/v2/modules/caddyevents/eventsconfig"
8 + _ "github.com/caddyserver/caddy/v2/modules/caddyfs"
9 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard"
10 _ "github.com/caddyserver/caddy/v2/modules/caddypki"
11 _ "github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver"