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"