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

This commit is contained in:
a 2024-01-13 14:12:43 -06:00 committed by GitHub
parent b359ca565c
commit c839a98ff5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 450 additions and 219 deletions

View File

@ -39,6 +39,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2/internal/filesystems"
"github.com/caddyserver/caddy/v2/notify" "github.com/caddyserver/caddy/v2/notify"
) )
@ -84,6 +85,9 @@ type Config struct {
storage certmagic.Storage storage certmagic.Storage
cancelFunc context.CancelFunc cancelFunc context.CancelFunc
// filesystems is a dict of filesystems that will later be loaded from and added to.
filesystems FileSystems
} }
// App is a thing that Caddy runs. // App is a thing that Caddy runs.
@ -447,6 +451,9 @@ func run(newCfg *Config, start bool) (Context, error) {
} }
} }
// create the new filesystem map
newCfg.filesystems = &filesystems.FilesystemMap{}
// prepare the new config for use // prepare the new config for use
newCfg.apps = make(map[string]App) newCfg.apps = make(map[string]App)

View File

@ -305,7 +305,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err) t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
} }
var ErrBarIsFull = errors.New("bar is full") ErrBarIsFull := errors.New("bar is full")
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull) bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
if !errors.Is(bookingError, ErrBarIsFull) { if !errors.Is(bookingError, ErrBarIsFull) {
t.Errorf("Errf(): should be able to unwrap the error chain") t.Errorf("Errf(): should be able to unwrap the error chain")

View File

@ -22,7 +22,7 @@ import (
) )
func TestParseVariadic(t *testing.T) { func TestParseVariadic(t *testing.T) {
var args = make([]string, 10) args := make([]string, 10)
for i, tc := range []struct { for i, tc := range []struct {
input string input string
result bool result bool
@ -111,7 +111,6 @@ func TestAllTokens(t *testing.T) {
input := []byte("a b c\nd e") input := []byte("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"} expected := []string{"a", "b", "c", "d", "e"}
tokens, err := allTokens("TestAllTokens", input) tokens, err := allTokens("TestAllTokens", input)
if err != nil { if err != nil {
t.Fatalf("Expected no error, got %v", err) t.Fatalf("Expected no error, got %v", err)
} }
@ -149,10 +148,11 @@ func TestParseOneAndImport(t *testing.T) {
"localhost", "localhost",
}, []int{1}}, }, []int{1}},
{`localhost:1234 {
`localhost:1234
dir1 foo bar`, false, []string{ dir1 foo bar`, false, []string{
"localhost:1234", "localhost:1234",
}, []int{3}, }, []int{3},
}, },
{`localhost { {`localhost {
@ -407,13 +407,13 @@ func TestRecursiveImport(t *testing.T) {
err = os.WriteFile(recursiveFile1, []byte( err = os.WriteFile(recursiveFile1, []byte(
`localhost `localhost
dir1 dir1
import recursive_import_test2`), 0644) import recursive_import_test2`), 0o644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.Remove(recursiveFile1) defer os.Remove(recursiveFile1)
err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0644) err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0o644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -441,7 +441,7 @@ func TestRecursiveImport(t *testing.T) {
err = os.WriteFile(recursiveFile1, []byte( err = os.WriteFile(recursiveFile1, []byte(
`localhost `localhost
dir1 dir1
import `+recursiveFile2), 0644) import `+recursiveFile2), 0o644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -495,7 +495,7 @@ func TestDirectiveImport(t *testing.T) {
} }
err = os.WriteFile(directiveFile, []byte(`prop1 1 err = os.WriteFile(directiveFile, []byte(`prop1 1
prop2 2`), 0644) prop2 2`), 0o644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -40,6 +40,7 @@ import (
func init() { func init() {
RegisterDirective("bind", parseBind) RegisterDirective("bind", parseBind)
RegisterDirective("tls", parseTLS) RegisterDirective("tls", parseTLS)
RegisterHandlerDirective("fs", parseFilesystem)
RegisterHandlerDirective("root", parseRoot) RegisterHandlerDirective("root", parseRoot)
RegisterHandlerDirective("vars", parseVars) RegisterHandlerDirective("vars", parseVars)
RegisterHandlerDirective("redir", parseRedir) RegisterHandlerDirective("redir", parseRedir)
@ -658,6 +659,23 @@ func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
return caddyhttp.VarsMiddleware{"root": root}, nil return caddyhttp.VarsMiddleware{"root": root}, nil
} }
// parseFilesystem parses the fs directive. Syntax:
//
// fs <filesystem>
func parseFilesystem(h Helper) (caddyhttp.MiddlewareHandler, error) {
var name string
for h.Next() {
if !h.NextArg() {
return nil, h.ArgErr()
}
name = h.Val()
if h.NextArg() {
return nil, h.ArgErr()
}
}
return caddyhttp.VarsMiddleware{"fs": name}, nil
}
// parseVars parses the vars directive. See its UnmarshalCaddyfile method for syntax. // parseVars parses the vars directive. See its UnmarshalCaddyfile method for syntax.
func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) { func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) {
v := new(caddyhttp.VarsMiddleware) v := new(caddyhttp.VarsMiddleware)

View File

@ -41,6 +41,7 @@ var directiveOrder = []string{
"map", "map",
"vars", "vars",
"fs",
"root", "root",
"skip_log", "skip_log",

View File

@ -31,20 +31,23 @@ func TestHostsFromKeys(t *testing.T) {
[]Address{ []Address{
{Original: ":2015", Port: "2015"}, {Original: ":2015", Port: "2015"},
}, },
[]string{}, []string{}, []string{},
[]string{},
}, },
{ {
[]Address{ []Address{
{Original: ":443", Port: "443"}, {Original: ":443", Port: "443"},
}, },
[]string{}, []string{}, []string{},
[]string{},
}, },
{ {
[]Address{ []Address{
{Original: "foo", Host: "foo"}, {Original: "foo", Host: "foo"},
{Original: ":2015", Port: "2015"}, {Original: ":2015", Port: "2015"},
}, },
[]string{}, []string{"foo"}, []string{},
[]string{"foo"},
}, },
{ {
[]Address{ []Address{

View File

@ -271,6 +271,12 @@ func (st ServerType) Setup(
if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) { if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) {
cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings) cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings)
} }
if filesystems, ok := options["filesystem"].(caddy.Module); ok {
cfg.AppsRaw["caddy.filesystems"] = caddyconfig.JSON(
filesystems,
&warnings)
}
if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok { if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr, cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
"module", "module",
@ -280,7 +286,6 @@ func (st ServerType) Setup(
if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil { if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
cfg.Admin = adminConfig cfg.Admin = adminConfig
} }
if pc, ok := options["persist_config"].(string); ok && pc == "off" { if pc, ok := options["persist_config"].(string); ok && pc == "off" {
if cfg.Admin == nil { if cfg.Admin == nil {
cfg.Admin = new(caddy.AdminConfig) cfg.Admin = new(caddy.AdminConfig)

View File

@ -9,7 +9,6 @@ import (
) )
func TestRespond(t *testing.T) { func TestRespond(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
@ -32,7 +31,6 @@ func TestRespond(t *testing.T) {
} }
func TestRedirect(t *testing.T) { func TestRedirect(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
@ -61,7 +59,6 @@ func TestRedirect(t *testing.T) {
} }
func TestDuplicateHosts(t *testing.T) { func TestDuplicateHosts(t *testing.T) {
// act and assert // act and assert
caddytest.AssertLoadError(t, caddytest.AssertLoadError(t,
` `
@ -76,7 +73,6 @@ func TestDuplicateHosts(t *testing.T) {
} }
func TestReadCookie(t *testing.T) { func TestReadCookie(t *testing.T) {
localhost, _ := url.Parse("http://localhost") localhost, _ := url.Parse("http://localhost")
cookie := http.Cookie{ cookie := http.Cookie{
Name: "clientname", Name: "clientname",
@ -110,7 +106,6 @@ func TestReadCookie(t *testing.T) {
} }
func TestReplIndex(t *testing.T) { func TestReplIndex(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
{ {

View File

@ -57,7 +57,6 @@ func TestSRVReverseProxy(t *testing.T) {
} }
func TestDialWithPlaceholderUnix(t *testing.T) { func TestDialWithPlaceholderUnix(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.SkipNow() t.SkipNow()
} }

View File

@ -7,7 +7,6 @@ import (
) )
func TestDefaultSNI(t *testing.T) { func TestDefaultSNI(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(`{ tester.InitServer(`{
@ -107,7 +106,6 @@ func TestDefaultSNI(t *testing.T) {
} }
func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) {
// arrange // arrange
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`

View File

@ -360,7 +360,6 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server { func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host != "127.0.0.1:9443" { if r.Host != "127.0.0.1:9443" {
t.Errorf("r.Host doesn't match, %v!", r.Host) t.Errorf("r.Host doesn't match, %v!", r.Host)
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)

View File

@ -23,6 +23,8 @@ import (
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2/internal/filesystems"
) )
// Context is a type which defines the lifetime of modules that // Context is a type which defines the lifetime of modules that
@ -37,6 +39,7 @@ import (
// not actually need to do this). // not actually need to do this).
type Context struct { type Context struct {
context.Context context.Context
moduleInstances map[string][]Module moduleInstances map[string][]Module
cfg *Config cfg *Config
cleanupFuncs []func() cleanupFuncs []func()
@ -81,6 +84,15 @@ func (ctx *Context) OnCancel(f func()) {
ctx.cleanupFuncs = append(ctx.cleanupFuncs, f) ctx.cleanupFuncs = append(ctx.cleanupFuncs, f)
} }
// Filesystems returns a ref to the FilesystemMap
func (ctx *Context) Filesystems() FileSystems {
// if no config is loaded, we use a default filesystemmap, which includes the osfs
if ctx.cfg == nil {
return &filesystems.FilesystemMap{}
}
return ctx.cfg.filesystems
}
// LoadModule loads the Caddy module(s) from the specified field of the parent struct // LoadModule loads the Caddy module(s) from the specified field of the parent struct
// pointer and returns the loaded module(s). The struct pointer and its field name as // pointer and returns the loaded module(s). The struct pointer and its field name as
// a string are necessary so that reflection can be used to read the struct tag on the // a string are necessary so that reflection can be used to read the struct tag on the

10
filesystem.go Normal file
View File

@ -0,0 +1,10 @@
package caddy
import "io/fs"
type FileSystems interface {
Register(k string, v fs.FS)
Unregister(k string)
Get(k string) (v fs.FS, ok bool)
Default() fs.FS
}

View File

@ -0,0 +1,77 @@
package filesystems
import (
"io/fs"
"strings"
"sync"
)
const (
DefaultFilesystemKey = "default"
)
var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}}
// wrapperFs exists so can easily add to wrapperFs down the line
type wrapperFs struct {
key string
fs.FS
}
// FilesystemMap stores a map of filesystems
// the empty key will be overwritten to be the default key
// it includes a default filesystem, based off the os fs
type FilesystemMap struct {
m sync.Map
}
// note that the first invocation of key cannot be called in a racy context.
func (f *FilesystemMap) key(k string) string {
if k == "" {
k = DefaultFilesystemKey
}
return k
}
// Register will add the filesystem with key to later be retrieved
// A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil
func (f *FilesystemMap) Register(k string, v fs.FS) {
k = f.key(k)
if v == nil {
f.Unregister(k)
return
}
f.m.Store(k, &wrapperFs{key: k, FS: v})
}
// Unregister will remove the filesystem with key from the filesystem map
// if the key is the default key, it will set the default to the osFS instead of deleting it
// modules should call this on cleanup to be safe
func (f *FilesystemMap) Unregister(k string) {
k = f.key(k)
if k == DefaultFilesystemKey {
f.m.Store(k, DefaultFilesystem)
} else {
f.m.Delete(k)
}
}
// Get will get a filesystem with a given key
func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
k = f.key(k)
c, ok := f.m.Load(strings.TrimSpace(k))
if !ok {
if k == DefaultFilesystemKey {
f.m.Store(k, DefaultFilesystem)
return DefaultFilesystem, true
}
return nil, ok
}
return c.(fs.FS), true
}
// Default will get the default filesystem in the filesystem map
func (f *FilesystemMap) Default() fs.FS {
val, _ := f.Get(DefaultFilesystemKey)
return val
}

View File

@ -0,0 +1,29 @@
package filesystems
import (
"io/fs"
"os"
"path/filepath"
)
// OsFS is a simple fs.FS implementation that uses the local
// file system. (We do not use os.DirFS because we do our own
// rooting or path prefixing without being constrained to a single
// root folder. The standard os.DirFS implementation is problematic
// since roots can be dynamic in our application.)
//
// OsFS also implements fs.StatFS, fs.GlobFS, fs.ReadDirFS, and fs.ReadFileFS.
type OsFS struct{}
func (OsFS) Open(name string) (fs.File, error) { return os.Open(name) }
func (OsFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
func (OsFS) Glob(pattern string) ([]string, error) { return filepath.Glob(pattern) }
func (OsFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(name) }
func (OsFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) }
var (
_ fs.StatFS = (*OsFS)(nil)
_ fs.GlobFS = (*OsFS)(nil)
_ fs.ReadDirFS = (*OsFS)(nil)
_ fs.ReadFileFS = (*OsFS)(nil)
)

View File

@ -0,0 +1,112 @@
package caddyfs
import (
"encoding/json"
"fmt"
"io/fs"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
)
func init() {
caddy.RegisterModule(Filesystems{})
httpcaddyfile.RegisterGlobalOption("filesystem", parseFilesystems)
}
type moduleEntry struct {
Key string `json:"name,omitempty"`
FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"`
fileSystem fs.FS
}
// Filesystems loads caddy.fs modules into the global filesystem map
type Filesystems struct {
Filesystems []*moduleEntry `json:"filesystems"`
defers []func()
}
func parseFilesystems(d *caddyfile.Dispenser, existingVal any) (any, error) {
p := &Filesystems{}
current, ok := existingVal.(*Filesystems)
if ok {
p = current
}
x := &moduleEntry{}
err := x.UnmarshalCaddyfile(d)
if err != nil {
return nil, err
}
p.Filesystems = append(p.Filesystems, x)
return p, nil
}
// CaddyModule returns the Caddy module information.
func (Filesystems) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.filesystems",
New: func() caddy.Module { return new(Filesystems) },
}
}
func (xs *Filesystems) Start() error { return nil }
func (xs *Filesystems) Stop() error { return nil }
func (xs *Filesystems) Provision(ctx caddy.Context) error {
// load the filesystem module
for _, f := range xs.Filesystems {
if len(f.FileSystemRaw) > 0 {
mod, err := ctx.LoadModule(f, "FileSystemRaw")
if err != nil {
return fmt.Errorf("loading file system module: %v", err)
}
f.fileSystem = mod.(fs.FS)
}
// register that module
ctx.Logger().Debug("registering fs", zap.String("fs", f.Key))
ctx.Filesystems().Register(f.Key, f.fileSystem)
// remember to unregister the module when we are done
xs.defers = append(xs.defers, func() {
ctx.Logger().Debug("registering fs", zap.String("fs", f.Key))
ctx.Filesystems().Unregister(f.Key)
})
}
return nil
}
func (f *Filesystems) Cleanup() error {
for _, v := range f.defers {
v()
}
return nil
}
func (f *moduleEntry) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
// key required for now
if !d.Args(&f.Key) {
return d.ArgErr()
}
// get the module json
if !d.NextArg() {
return d.ArgErr()
}
name := d.Val()
modID := "caddy.fs." + name
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
fsys, ok := unm.(fs.FS)
if !ok {
return d.Errf("module %s (%T) is not a supported file system implementation (requires fs.FS)", modID, unm)
}
f.FileSystemRaw = caddyconfig.JSONModuleObject(fsys, "backend", name, nil)
}
return nil
}

View File

@ -105,7 +105,6 @@ func TestPreferOrder(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
if test.accept == "" { if test.accept == "" {
r.Header.Del("Accept-Encoding") r.Header.Del("Accept-Encoding")
} else { } else {
@ -258,7 +257,6 @@ func TestValidate(t *testing.T) {
t.Errorf("Validate() error = %v, wantErr = %v", err, test.wantErr) t.Errorf("Validate() error = %v, wantErr = %v", err, test.wantErr)
} }
}) })
} }
} }

View File

@ -52,7 +52,7 @@ type Browse struct {
TemplateFile string `json:"template_file,omitempty"` TemplateFile string `json:"template_file,omitempty"`
} }
func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
fsrv.logger.Debug("browse enabled; listing directory contents", fsrv.logger.Debug("browse enabled; listing directory contents",
zap.String("path", dirPath), zap.String("path", dirPath),
zap.String("root", root)) zap.String("root", root))
@ -82,7 +82,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
} }
} }
dir, err := fsrv.openFile(dirPath, w) dir, err := fsrv.openFile(fileSystem, dirPath, w)
if err != nil { if err != nil {
return err return err
} }
@ -91,7 +91,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// 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 // 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
listing, err := fsrv.loadDirectoryContents(r.Context(), dir.(fs.ReadDirFile), root, path.Clean(r.URL.EscapedPath()), repl) listing, err := fsrv.loadDirectoryContents(r.Context(), fileSystem, dir.(fs.ReadDirFile), root, path.Clean(r.URL.EscapedPath()), repl)
switch { switch {
case errors.Is(err, fs.ErrPermission): case errors.Is(err, fs.ErrPermission):
return caddyhttp.Error(http.StatusForbidden, err) return caddyhttp.Error(http.StatusForbidden, err)
@ -145,7 +145,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
return nil return nil
} }
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) { func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs.FS, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) {
files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return nil, err return nil, err
@ -154,7 +154,7 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDi
// user can presumably browse "up" to parent folder if path is longer than "/" // user can presumably browse "up" to parent folder if path is longer than "/"
canGoUp := len(urlPath) > 1 canGoUp := len(urlPath) > 1
return fsrv.directoryListing(ctx, files, canGoUp, root, urlPath, repl), nil return fsrv.directoryListing(ctx, fileSystem, files, canGoUp, root, urlPath, repl), nil
} }
// browseApplyQueryParams applies query parameters to the listing. // browseApplyQueryParams applies query parameters to the listing.
@ -223,12 +223,12 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.T
// isSymlinkTargetDir returns true if f's symbolic link target // isSymlinkTargetDir returns true if f's symbolic link target
// is a directory. // is a directory.
func (fsrv *FileServer) isSymlinkTargetDir(f fs.FileInfo, root, urlPath string) bool { func (fsrv *FileServer) isSymlinkTargetDir(fileSystem fs.FS, f fs.FileInfo, root, urlPath string) bool {
if !isSymlink(f) { if !isSymlink(f) {
return false return false
} }
target := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name())) target := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name()))
targetInfo, err := fs.Stat(fsrv.fileSystem, target) targetInfo, err := fs.Stat(fileSystem, target)
if err != nil { if err != nil {
return false return false
} }

View File

@ -32,7 +32,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
) )
func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext { func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext {
filesToHide := fsrv.transformHidePaths(repl) filesToHide := fsrv.transformHidePaths(repl)
name, _ := url.PathUnescape(urlPath) name, _ := url.PathUnescape(urlPath)
@ -62,7 +62,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
continue continue
} }
isDir := entry.IsDir() || fsrv.isSymlinkTargetDir(info, root, urlPath) isDir := entry.IsDir() || fsrv.isSymlinkTargetDir(fileSystem, info, root, urlPath)
// add the slash after the escape of path to avoid escaping the slash as well // add the slash after the escape of path to avoid escaping the slash as well
if isDir { if isDir {
@ -76,7 +76,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
fileIsSymlink := isSymlink(info) fileIsSymlink := isSymlink(info)
if fileIsSymlink { if fileIsSymlink {
path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, info.Name())) path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, info.Name()))
fileInfo, err := fs.Stat(fsrv.fileSystem, path) fileInfo, err := fs.Stat(fileSystem, path)
if err == nil { if err == nil {
size = fileInfo.Size() size = fileInfo.Size()
} }

View File

@ -15,13 +15,11 @@
package fileserver package fileserver
import ( import (
"io/fs"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
@ -37,7 +35,7 @@ func init() {
// server and configures it with this syntax: // server and configures it with this syntax:
// //
// file_server [<matcher>] [browse] { // file_server [<matcher>] [browse] {
// fs <backend...> // fs <filesystem>
// root <path> // root <path>
// hide <files...> // hide <files...>
// index <files...> // index <files...>
@ -68,21 +66,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
if !h.NextArg() { if !h.NextArg() {
return nil, h.ArgErr() return nil, h.ArgErr()
} }
if fsrv.FileSystemRaw != nil { if fsrv.FileSystem != "" {
return nil, h.Err("file system module already specified") return nil, h.Err("file system already specified")
} }
name := h.Val() fsrv.FileSystem = h.Val()
modID := "caddy.fs." + name
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
fsys, ok := unm.(fs.FS)
if !ok {
return nil, h.Errf("module %s (%T) is not a supported file system implementation (requires fs.FS)", modID, unm)
}
fsrv.FileSystemRaw = caddyconfig.JSONModuleObject(fsys, "backend", name, nil)
case "hide": case "hide":
fsrv.Hide = h.RemainingArgs() fsrv.Hide = h.RemainingArgs()
if len(fsrv.Hide) == 0 { if len(fsrv.Hide) == 0 {

View File

@ -15,7 +15,6 @@
package fileserver package fileserver
import ( import (
"encoding/json"
"fmt" "fmt"
"io/fs" "io/fs"
"net/http" "net/http"
@ -64,8 +63,7 @@ func init() {
type MatchFile struct { type MatchFile struct {
// The file system implementation to use. By default, the // The file system implementation to use. By default, the
// local disk file system will be used. // local disk file system will be used.
FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"` FileSystem string `json:"fs,omitempty"`
fileSystem fs.FS
// The root directory, used for creating absolute // The root directory, used for creating absolute
// file paths, and required when working with // file paths, and required when working with
@ -108,6 +106,8 @@ type MatchFile struct {
// component in order to be used as a split delimiter. // component in order to be used as a split delimiter.
SplitPath []string `json:"split_path,omitempty"` SplitPath []string `json:"split_path,omitempty"`
fsmap caddy.FileSystems
logger *zap.Logger logger *zap.Logger
} }
@ -181,16 +181,22 @@ func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
root = values["root"][0] root = values["root"][0]
} }
var fsName string
if len(values["fs"]) > 0 {
fsName = values["fs"][0]
}
var try_policy string var try_policy string
if len(values["try_policy"]) > 0 { if len(values["try_policy"]) > 0 {
root = values["try_policy"][0] root = values["try_policy"][0]
} }
m := MatchFile{ m := MatchFile{
Root: root, Root: root,
TryFiles: values["try_files"], TryFiles: values["try_files"],
TryPolicy: try_policy, TryPolicy: try_policy,
SplitPath: values["split_path"], SplitPath: values["split_path"],
FileSystem: fsName,
} }
err = m.Provision(ctx) err = m.Provision(ctx)
@ -264,22 +270,16 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
func (m *MatchFile) Provision(ctx caddy.Context) error { func (m *MatchFile) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger() m.logger = ctx.Logger()
// establish the file system to use m.fsmap = ctx.Filesystems()
if len(m.FileSystemRaw) > 0 {
mod, err := ctx.LoadModule(m, "FileSystemRaw")
if err != nil {
return fmt.Errorf("loading file system module: %v", err)
}
m.fileSystem = mod.(fs.FS)
}
if m.fileSystem == nil {
m.fileSystem = osFS{}
}
if m.Root == "" { if m.Root == "" {
m.Root = "{http.vars.root}" m.Root = "{http.vars.root}"
} }
if m.FileSystem == "" {
m.FileSystem = "{http.vars.fs}"
}
// if list of files to try was omitted entirely, assume URL path // if list of files to try was omitted entirely, assume URL path
// (use placeholder instead of r.URL.Path; see issue #4146) // (use placeholder instead of r.URL.Path; see issue #4146)
if m.TryFiles == nil { if m.TryFiles == nil {
@ -320,6 +320,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
root := filepath.Clean(repl.ReplaceAll(m.Root, ".")) root := filepath.Clean(repl.ReplaceAll(m.Root, "."))
fsName := repl.ReplaceAll(m.FileSystem, "")
fileSystem, ok := m.fsmap.Get(fsName)
if !ok {
m.logger.Error("use of unregistered filesystem", zap.String("fs", fsName))
return false
}
type matchCandidate struct { type matchCandidate struct {
fullpath, relative, splitRemainder string fullpath, relative, splitRemainder string
} }
@ -368,7 +375,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
globResults = []string{fullPattern} // precious Windows globResults = []string{fullPattern} // precious Windows
} else { } else {
globResults, err = fs.Glob(m.fileSystem, fullPattern) globResults, err = fs.Glob(fileSystem, fullPattern)
if err != nil { if err != nil {
m.logger.Error("expanding glob", zap.Error(err)) m.logger.Error("expanding glob", zap.Error(err))
} }
@ -410,7 +417,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
} }
candidates := makeCandidates(pattern) candidates := makeCandidates(pattern)
for _, c := range candidates { for _, c := range candidates {
if info, exists := m.strictFileExists(c.fullpath); exists { if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists {
setPlaceholders(c, info) setPlaceholders(c, info)
return true return true
} }
@ -424,7 +431,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
for _, pattern := range m.TryFiles { for _, pattern := range m.TryFiles {
candidates := makeCandidates(pattern) candidates := makeCandidates(pattern)
for _, c := range candidates { for _, c := range candidates {
info, err := fs.Stat(m.fileSystem, c.fullpath) info, err := fs.Stat(fileSystem, c.fullpath)
if err == nil && info.Size() > largestSize { if err == nil && info.Size() > largestSize {
largestSize = info.Size() largestSize = info.Size()
largest = c largest = c
@ -445,7 +452,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
for _, pattern := range m.TryFiles { for _, pattern := range m.TryFiles {
candidates := makeCandidates(pattern) candidates := makeCandidates(pattern)
for _, c := range candidates { for _, c := range candidates {
info, err := fs.Stat(m.fileSystem, c.fullpath) info, err := fs.Stat(fileSystem, c.fullpath)
if err == nil && (smallestSize == 0 || info.Size() < smallestSize) { if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
smallestSize = info.Size() smallestSize = info.Size()
smallest = c smallest = c
@ -465,7 +472,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
for _, pattern := range m.TryFiles { for _, pattern := range m.TryFiles {
candidates := makeCandidates(pattern) candidates := makeCandidates(pattern)
for _, c := range candidates { for _, c := range candidates {
info, err := fs.Stat(m.fileSystem, c.fullpath) info, err := fs.Stat(fileSystem, c.fullpath)
if err == nil && if err == nil &&
(recentInfo == nil || info.ModTime().After(recentInfo.ModTime())) { (recentInfo == nil || info.ModTime().After(recentInfo.ModTime())) {
recent = c recent = c
@ -503,8 +510,8 @@ func parseErrorCode(input string) error {
// the file must also be a directory; if it does // the file must also be a directory; if it does
// NOT end in a forward slash, the file must NOT // NOT end in a forward slash, the file must NOT
// be a directory. // be a directory.
func (m MatchFile) strictFileExists(file string) (os.FileInfo, bool) { func (m MatchFile) strictFileExists(fileSystem fs.FS, file string) (os.FileInfo, bool) {
info, err := fs.Stat(m.fileSystem, file) info, err := fs.Stat(fileSystem, file)
if err != nil { if err != nil {
// in reality, this can be any error // in reality, this can be any error
// such as permission or even obscure // such as permission or even obscure

View File

@ -24,6 +24,7 @@ import (
"testing" "testing"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/internal/filesystems"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
) )
@ -116,9 +117,9 @@ func TestFileMatcher(t *testing.T) {
}, },
} { } {
m := &MatchFile{ m := &MatchFile{
fileSystem: osFS{}, fsmap: &filesystems.FilesystemMap{},
Root: "./testdata", Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
} }
u, err := url.Parse(tc.path) u, err := url.Parse(tc.path)
@ -225,10 +226,10 @@ func TestPHPFileMatcher(t *testing.T) {
}, },
} { } {
m := &MatchFile{ m := &MatchFile{
fileSystem: osFS{}, fsmap: &filesystems.FilesystemMap{},
Root: "./testdata", Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"}, TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"},
SplitPath: []string{".php"}, SplitPath: []string{".php"},
} }
u, err := url.Parse(tc.path) u, err := url.Parse(tc.path)
@ -264,7 +265,10 @@ func TestPHPFileMatcher(t *testing.T) {
} }
func TestFirstSplit(t *testing.T) { func TestFirstSplit(t *testing.T) {
m := MatchFile{SplitPath: []string{".php"}} m := MatchFile{
SplitPath: []string{".php"},
fsmap: &filesystems.FilesystemMap{},
}
actual, remainder := m.firstSplit("index.PHP/somewhere") actual, remainder := m.firstSplit("index.PHP/somewhere")
expected := "index.PHP" expected := "index.PHP"
expectedRemainder := "/somewhere" expectedRemainder := "/somewhere"
@ -276,83 +280,81 @@ func TestFirstSplit(t *testing.T) {
} }
} }
var ( var expressionTests = []struct {
expressionTests = []struct { name string
name string expression *caddyhttp.MatchExpression
expression *caddyhttp.MatchExpression urlTarget string
urlTarget string httpMethod string
httpMethod string httpHeader *http.Header
httpHeader *http.Header wantErr bool
wantErr bool wantResult bool
wantResult bool clientCertificate []byte
clientCertificate []byte }{
}{ {
{ name: "file error no args (MatchFile)",
name: "file error no args (MatchFile)", expression: &caddyhttp.MatchExpression{
expression: &caddyhttp.MatchExpression{ Expr: `file()`,
Expr: `file()`,
},
urlTarget: "https://example.com/foo.txt",
wantResult: true,
}, },
{ urlTarget: "https://example.com/foo.txt",
name: "file error bad try files (MatchFile)", wantResult: true,
expression: &caddyhttp.MatchExpression{ },
Expr: `file({"try_file": ["bad_arg"]})`, {
}, name: "file error bad try files (MatchFile)",
urlTarget: "https://example.com/foo", expression: &caddyhttp.MatchExpression{
wantErr: true, Expr: `file({"try_file": ["bad_arg"]})`,
}, },
{ urlTarget: "https://example.com/foo",
name: "file match short pattern index.php (MatchFile)", wantErr: true,
expression: &caddyhttp.MatchExpression{ },
Expr: `file("index.php")`, {
}, name: "file match short pattern index.php (MatchFile)",
urlTarget: "https://example.com/foo", expression: &caddyhttp.MatchExpression{
wantResult: true, Expr: `file("index.php")`,
}, },
{ urlTarget: "https://example.com/foo",
name: "file match short pattern foo.txt (MatchFile)", wantResult: true,
expression: &caddyhttp.MatchExpression{ },
Expr: `file({http.request.uri.path})`, {
}, name: "file match short pattern foo.txt (MatchFile)",
urlTarget: "https://example.com/foo.txt", expression: &caddyhttp.MatchExpression{
wantResult: true, Expr: `file({http.request.uri.path})`,
}, },
{ urlTarget: "https://example.com/foo.txt",
name: "file match index.php (MatchFile)", wantResult: true,
expression: &caddyhttp.MatchExpression{ },
Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`, {
}, name: "file match index.php (MatchFile)",
urlTarget: "https://example.com/foo", expression: &caddyhttp.MatchExpression{
wantResult: true, Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`,
}, },
{ urlTarget: "https://example.com/foo",
name: "file match long pattern foo.txt (MatchFile)", wantResult: true,
expression: &caddyhttp.MatchExpression{ },
Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`, {
}, name: "file match long pattern foo.txt (MatchFile)",
urlTarget: "https://example.com/foo.txt", expression: &caddyhttp.MatchExpression{
wantResult: true, Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
}, },
{ urlTarget: "https://example.com/foo.txt",
name: "file match long pattern foo.txt with concatenation (MatchFile)", wantResult: true,
expression: &caddyhttp.MatchExpression{ },
Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`, {
}, name: "file match long pattern foo.txt with concatenation (MatchFile)",
urlTarget: "https://example.com/foo.txt", expression: &caddyhttp.MatchExpression{
wantResult: true, Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`,
}, },
{ urlTarget: "https://example.com/foo.txt",
name: "file not match long pattern (MatchFile)", wantResult: true,
expression: &caddyhttp.MatchExpression{ },
Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`, {
}, name: "file not match long pattern (MatchFile)",
urlTarget: "https://example.com/nopenope.txt", expression: &caddyhttp.MatchExpression{
wantResult: false, Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
}, },
} urlTarget: "https://example.com/nopenope.txt",
) wantResult: false,
},
}
func TestMatchExpressionMatch(t *testing.T) { func TestMatchExpressionMatch(t *testing.T) {
for _, tst := range expressionTests { for _, tst := range expressionTests {

View File

@ -15,7 +15,6 @@
package fileserver package fileserver
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -97,15 +96,8 @@ type FileServer struct {
// The file system implementation to use. By default, Caddy uses the local // The file system implementation to use. By default, Caddy uses the local
// disk file system. // disk file system.
// //
// File system modules used here must adhere to the following requirements: // if a non default filesystem is used, it must be first be registered in the globals section.
// - Implement fs.FS interface. FileSystem string `json:"fs,omitempty"`
// - Support seeking on opened files; i.e.returned fs.File values must
// implement the io.Seeker interface. This is required for determining
// Content-Length and satisfying Range requests.
// - fs.File values that represent directories must implement the
// fs.ReadDirFile interface so that directory listings can be procured.
FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"`
fileSystem fs.FS
// The path to the root of the site. Default is `{http.vars.root}` if set, // The path to the root of the site. Default is `{http.vars.root}` if set,
// or current working directory otherwise. This should be a trusted value. // or current working directory otherwise. This should be a trusted value.
@ -169,6 +161,8 @@ type FileServer struct {
PrecompressedOrder []string `json:"precompressed_order,omitempty"` PrecompressedOrder []string `json:"precompressed_order,omitempty"`
precompressors map[string]encode.Precompressed precompressors map[string]encode.Precompressed
fsmap caddy.FileSystems
logger *zap.Logger logger *zap.Logger
} }
@ -184,16 +178,10 @@ func (FileServer) CaddyModule() caddy.ModuleInfo {
func (fsrv *FileServer) Provision(ctx caddy.Context) error { func (fsrv *FileServer) Provision(ctx caddy.Context) error {
fsrv.logger = ctx.Logger() fsrv.logger = ctx.Logger()
// establish which file system (possibly a virtual one) we'll be using fsrv.fsmap = ctx.Filesystems()
if len(fsrv.FileSystemRaw) > 0 {
mod, err := ctx.LoadModule(fsrv, "FileSystemRaw") if fsrv.FileSystem == "" {
if err != nil { fsrv.FileSystem = "{http.vars.fs}"
return fmt.Errorf("loading file system module: %v", err)
}
fsrv.fileSystem = mod.(fs.FS)
}
if fsrv.fileSystem == nil {
fsrv.fileSystem = osFS{}
} }
if fsrv.Root == "" { if fsrv.Root == "" {
@ -263,19 +251,26 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
filesToHide := fsrv.transformHidePaths(repl) filesToHide := fsrv.transformHidePaths(repl)
root := repl.ReplaceAll(fsrv.Root, ".") root := repl.ReplaceAll(fsrv.Root, ".")
fsName := repl.ReplaceAll(fsrv.FileSystem, "")
fileSystem, ok := fsrv.fsmap.Get(fsName)
if !ok {
return caddyhttp.Error(http.StatusNotFound, fmt.Errorf("filesystem not found"))
}
// remove any trailing `/` as it breaks fs.ValidPath() in the stdlib // remove any trailing `/` as it breaks fs.ValidPath() in the stdlib
filename := strings.TrimSuffix(caddyhttp.SanitizedPathJoin(root, r.URL.Path), "/") filename := strings.TrimSuffix(caddyhttp.SanitizedPathJoin(root, r.URL.Path), "/")
fsrv.logger.Debug("sanitized path join", fsrv.logger.Debug("sanitized path join",
zap.String("site_root", root), zap.String("site_root", root),
zap.String("fs", fsName),
zap.String("request_path", r.URL.Path), zap.String("request_path", r.URL.Path),
zap.String("result", filename)) zap.String("result", filename))
// get information about the file // get information about the file
info, err := fs.Stat(fsrv.fileSystem, filename) info, err := fs.Stat(fileSystem, filename)
if err != nil { if err != nil {
err = fsrv.mapDirOpenError(err, filename) err = fsrv.mapDirOpenError(fileSystem, err, filename)
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) { if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) {
return fsrv.notFound(w, r, next) return fsrv.notFound(w, r, next)
} else if errors.Is(err, fs.ErrPermission) { } else if errors.Is(err, fs.ErrPermission) {
@ -299,7 +294,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
continue continue
} }
indexInfo, err := fs.Stat(fsrv.fileSystem, indexPath) indexInfo, err := fs.Stat(fileSystem, indexPath)
if err != nil { if err != nil {
continue continue
} }
@ -327,7 +322,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
zap.String("path", filename), zap.String("path", filename),
zap.Strings("index_filenames", fsrv.IndexNames)) zap.Strings("index_filenames", fsrv.IndexNames))
if fsrv.Browse != nil && !fileHidden(filename, filesToHide) { if fsrv.Browse != nil && !fileHidden(filename, filesToHide) {
return fsrv.serveBrowse(root, filename, w, r, next) return fsrv.serveBrowse(fileSystem, root, filename, w, r, next)
} }
return fsrv.notFound(w, r, next) return fsrv.notFound(w, r, next)
} }
@ -381,13 +376,13 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
continue continue
} }
compressedFilename := filename + precompress.Suffix() compressedFilename := filename + precompress.Suffix()
compressedInfo, err := fs.Stat(fsrv.fileSystem, compressedFilename) compressedInfo, err := fs.Stat(fileSystem, compressedFilename)
if err != nil || compressedInfo.IsDir() { if err != nil || compressedInfo.IsDir() {
fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err)) fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err))
continue continue
} }
fsrv.logger.Debug("opening compressed sidecar file", zap.String("filename", compressedFilename), zap.Error(err)) fsrv.logger.Debug("opening compressed sidecar file", zap.String("filename", compressedFilename), zap.Error(err))
file, err = fsrv.openFile(compressedFilename, w) file, err = fsrv.openFile(fileSystem, compressedFilename, w)
if err != nil { if err != nil {
fsrv.logger.Warn("opening precompressed file failed", zap.String("filename", compressedFilename), zap.Error(err)) fsrv.logger.Warn("opening precompressed file failed", zap.String("filename", compressedFilename), zap.Error(err))
if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable { 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
fsrv.logger.Debug("opening file", zap.String("filename", filename)) fsrv.logger.Debug("opening file", zap.String("filename", filename))
// open the file // open the file
file, err = fsrv.openFile(filename, w) file, err = fsrv.openFile(fileSystem, filename, w)
if err != nil { if err != nil {
if herr, ok := err.(caddyhttp.HandlerError); ok && if herr, ok := err.(caddyhttp.HandlerError); ok &&
herr.StatusCode == http.StatusNotFound { herr.StatusCode == http.StatusNotFound {
@ -502,10 +497,10 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
// the response is configured to inform the client how to best handle it // the response is configured to inform the client how to best handle it
// and a well-described handler error is returned (do not wrap the // and a well-described handler error is returned (do not wrap the
// returned error value). // returned error value).
func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (fs.File, error) { func (fsrv *FileServer) openFile(fileSystem fs.FS, filename string, w http.ResponseWriter) (fs.File, error) {
file, err := fsrv.fileSystem.Open(filename) file, err := fileSystem.Open(filename)
if err != nil { if err != nil {
err = fsrv.mapDirOpenError(err, filename) err = fsrv.mapDirOpenError(fileSystem, err, filename)
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
fsrv.logger.Debug("file not found", zap.String("filename", filename), zap.Error(err)) fsrv.logger.Debug("file not found", zap.String("filename", filename), zap.Error(err))
return nil, caddyhttp.Error(http.StatusNotFound, err) return nil, caddyhttp.Error(http.StatusNotFound, err)
@ -530,7 +525,7 @@ func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (fs.Fil
// Adapted from the Go standard library; originally written by Nathaniel Caza. // Adapted from the Go standard library; originally written by Nathaniel Caza.
// https://go-review.googlesource.com/c/go/+/36635/ // https://go-review.googlesource.com/c/go/+/36635/
// https://go-review.googlesource.com/c/go/+/36804/ // https://go-review.googlesource.com/c/go/+/36804/
func (fsrv *FileServer) mapDirOpenError(originalErr error, name string) error { func (fsrv *FileServer) mapDirOpenError(fileSystem fs.FS, originalErr error, name string) error {
if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) { if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) {
return originalErr return originalErr
} }
@ -540,7 +535,7 @@ func (fsrv *FileServer) mapDirOpenError(originalErr error, name string) error {
if parts[i] == "" { if parts[i] == "" {
continue continue
} }
fi, err := fs.Stat(fsrv.fileSystem, strings.Join(parts[:i+1], separator)) fi, err := fs.Stat(fileSystem, strings.Join(parts[:i+1], separator))
if err != nil { if err != nil {
return originalErr return originalErr
} }
@ -673,21 +668,6 @@ func (wr statusOverrideResponseWriter) Unwrap() http.ResponseWriter {
return wr.ResponseWriter return wr.ResponseWriter
} }
// osFS is a simple fs.FS implementation that uses the local
// file system. (We do not use os.DirFS because we do our own
// rooting or path prefixing without being constrained to a single
// root folder. The standard os.DirFS implementation is problematic
// since roots can be dynamic in our application.)
//
// osFS also implements fs.StatFS, fs.GlobFS, fs.ReadDirFS, and fs.ReadFileFS.
type osFS struct{}
func (osFS) Open(name string) (fs.File, error) { return os.Open(name) }
func (osFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
func (osFS) Glob(pattern string) ([]string, error) { return filepath.Glob(pattern) }
func (osFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(name) }
func (osFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) }
var defaultIndexNames = []string{"index.html", "index.txt"} var defaultIndexNames = []string{"index.html", "index.txt"}
const ( const (
@ -699,9 +679,4 @@ const (
var ( var (
_ caddy.Provisioner = (*FileServer)(nil) _ caddy.Provisioner = (*FileServer)(nil)
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil) _ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
_ fs.StatFS = (*osFS)(nil)
_ fs.GlobFS = (*osFS)(nil)
_ fs.ReadDirFS = (*osFS)(nil)
_ fs.ReadFileFS = (*osFS)(nil)
) )

View File

@ -862,7 +862,6 @@ func TestHeaderREMatcher(t *testing.T) {
} }
func BenchmarkHeaderREMatcher(b *testing.B) { func BenchmarkHeaderREMatcher(b *testing.B) {
i := 0 i := 0
match := MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}} match := MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}}
input := http.Header{"Field": []string{"foobar"}} input := http.Header{"Field": []string{"foobar"}}
@ -1086,6 +1085,7 @@ func TestNotMatcher(t *testing.T) {
} }
} }
} }
func BenchmarkLargeHostMatcher(b *testing.B) { func BenchmarkLargeHostMatcher(b *testing.B) {
// this benchmark simulates a large host matcher (thousands of entries) where each // this benchmark simulates a large host matcher (thousands of entries) where each
// value is an exact hostname (not a placeholder or wildcard) - compare the results // value is an exact hostname (not a placeholder or wildcard) - compare the results

View File

@ -26,7 +26,7 @@ package reverseproxy
import "testing" import "testing"
func TestEqualFold(t *testing.T) { func TestEqualFold(t *testing.T) {
var tests = []struct { tests := []struct {
name string name string
a, b string a, b string
want bool want bool
@ -64,7 +64,7 @@ func TestEqualFold(t *testing.T) {
} }
func TestIsPrint(t *testing.T) { func TestIsPrint(t *testing.T) {
var tests = []struct { tests := []struct {
name string name string
in string in string
want bool want bool

View File

@ -48,7 +48,7 @@ import (
// and output "FAILED" in response // and output "FAILED" in response
const ( const (
scriptFile = "/tank/www/fcgic_test.php" scriptFile = "/tank/www/fcgic_test.php"
//ipPort = "remote-php-serv:59000" // ipPort = "remote-php-serv:59000"
ipPort = "127.0.0.1:59000" ipPort = "127.0.0.1:59000"
) )
@ -57,7 +57,6 @@ var globalt *testing.T
type FastCGIServer struct{} type FastCGIServer struct{}
func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
if err := req.ParseMultipartForm(100000000); err != nil { if err := req.ParseMultipartForm(100000000); err != nil {
log.Printf("[ERROR] failed to parse: %v", err) log.Printf("[ERROR] failed to parse: %v", err)
} }
@ -84,7 +83,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
if req.MultipartForm != nil { if req.MultipartForm != nil {
fileNum = len(req.MultipartForm.File) fileNum = len(req.MultipartForm.File)
for kn, fns := range req.MultipartForm.File { for kn, fns := range req.MultipartForm.File {
//fmt.Fprintln(resp, "server:filekey ", kn ) // fmt.Fprintln(resp, "server:filekey ", kn )
length += len(kn) length += len(kn)
for _, f := range fns { for _, f := range fns {
fd, err := f.Open() fd, err := f.Open()
@ -101,13 +100,13 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
length += int(l0) length += int(l0)
defer fd.Close() defer fd.Close()
md5 := fmt.Sprintf("%x", h.Sum(nil)) md5 := fmt.Sprintf("%x", h.Sum(nil))
//fmt.Fprintln(resp, "server:filemd5 ", md5 ) // fmt.Fprintln(resp, "server:filemd5 ", md5 )
if kn != md5 { if kn != md5 {
fmt.Fprintln(resp, "server:err ", md5, kn) fmt.Fprintln(resp, "server:err ", md5, kn)
stat = "FAILED" stat = "FAILED"
} }
//fmt.Fprintln(resp, "server:filename ", f.Filename ) // fmt.Fprintln(resp, "server:filename ", f.Filename )
} }
} }
} }
@ -181,7 +180,6 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
} }
func generateRandFile(size int) (p string, m string) { func generateRandFile(size int) (p string, m string) {
p = filepath.Join(os.TempDir(), "fcgict"+strconv.Itoa(rand.Int())) p = filepath.Join(os.TempDir(), "fcgict"+strconv.Itoa(rand.Int()))
// open output file // open output file
@ -236,7 +234,7 @@ func DisabledTest(t *testing.T) {
fcgiParams := make(map[string]string) fcgiParams := make(map[string]string)
fcgiParams["REQUEST_METHOD"] = "GET" fcgiParams["REQUEST_METHOD"] = "GET"
fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1" fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1"
//fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1" // fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1"
fcgiParams["SCRIPT_FILENAME"] = scriptFile fcgiParams["SCRIPT_FILENAME"] = scriptFile
// simple GET // simple GET

View File

@ -629,7 +629,6 @@ func TestRandomChoicePolicy(t *testing.T) {
if h == pool[0] { if h == pool[0] {
t.Error("RandomChoicePolicy should not choose pool[0]") t.Error("RandomChoicePolicy should not choose pool[0]")
} }
} }
func TestCookieHashPolicy(t *testing.T) { func TestCookieHashPolicy(t *testing.T) {

View File

@ -333,7 +333,7 @@ func TestRewrite(t *testing.T) {
input: newRequest(t, "GET", "/foo/findme%2Fbar"), input: newRequest(t, "GET", "/foo/findme%2Fbar"),
expect: newRequest(t, "GET", "/foo/replaced%2Fbar"), expect: newRequest(t, "GET", "/foo/replaced%2Fbar"),
}, },
{ {
rule: Rewrite{PathRegexp: []*regexReplacer{{Find: "/{2,}", Replace: "/"}}}, rule: Rewrite{PathRegexp: []*regexReplacer{{Find: "/{2,}", Replace: "/"}}},
input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"), input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"),

View File

@ -28,7 +28,6 @@ func Test_tracersProvider_cleanupTracerProvider(t *testing.T) {
tp.getTracerProvider() tp.getTracerProvider()
err := tp.cleanupTracerProvider(zap.NewNop()) err := tp.cleanupTracerProvider(zap.NewNop())
if err != nil { if err != nil {
t.Errorf("There should be no error: %v", err) t.Errorf("There should be no error: %v", err)
} }

View File

@ -5,6 +5,7 @@ import (
_ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" _ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
_ "github.com/caddyserver/caddy/v2/modules/caddyevents" _ "github.com/caddyserver/caddy/v2/modules/caddyevents"
_ "github.com/caddyserver/caddy/v2/modules/caddyevents/eventsconfig" _ "github.com/caddyserver/caddy/v2/modules/caddyevents/eventsconfig"
_ "github.com/caddyserver/caddy/v2/modules/caddyfs"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard"
_ "github.com/caddyserver/caddy/v2/modules/caddypki" _ "github.com/caddyserver/caddy/v2/modules/caddypki"
_ "github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver" _ "github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver"