mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 02:27:19 -04:00 
			
		
		
		
	filesystem: Globally declared filesystems, fs directive (#5833)
				
					
				
			This commit is contained in:
		
							parent
							
								
									b359ca565c
								
							
						
					
					
						commit
						c839a98ff5
					
				
							
								
								
									
										7
									
								
								caddy.go
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								caddy.go
									
									
									
									
									
								
							| @ -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) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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") | ||||||
|  | |||||||
| @ -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,7 +148,8 @@ 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}, | ||||||
| @ -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) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -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) | ||||||
|  | |||||||
| @ -41,6 +41,7 @@ var directiveOrder = []string{ | |||||||
| 
 | 
 | ||||||
| 	"map", | 	"map", | ||||||
| 	"vars", | 	"vars", | ||||||
|  | 	"fs", | ||||||
| 	"root", | 	"root", | ||||||
| 	"skip_log", | 	"skip_log", | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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{ | ||||||
|  | |||||||
| @ -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) | ||||||
|  | |||||||
| @ -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(` | ||||||
|   { |   { | ||||||
|  | |||||||
| @ -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() | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -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(`  | ||||||
|  | |||||||
| @ -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) | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								context.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								context.go
									
									
									
									
									
								
							| @ -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
									
								
							
							
						
						
									
										10
									
								
								filesystem.go
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
							
								
								
									
										77
									
								
								internal/filesystems/map.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								internal/filesystems/map.go
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								internal/filesystems/os.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								internal/filesystems/os.go
									
									
									
									
									
										Normal 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) | ||||||
|  | ) | ||||||
							
								
								
									
										112
									
								
								modules/caddyfs/filesystem.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								modules/caddyfs/filesystem.go
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
| @ -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) | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 
 |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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 | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -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() | ||||||
| 			} | 			} | ||||||
|  | |||||||
| @ -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 { | ||||||
|  | |||||||
| @ -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,6 +181,11 @@ 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] | ||||||
| @ -191,6 +196,7 @@ func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) { | |||||||
| 			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 | ||||||
|  | |||||||
| @ -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,7 +117,7 @@ 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}/"}, | ||||||
| 		} | 		} | ||||||
| @ -225,7 +226,7 @@ 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"}, | ||||||
| @ -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,8 +280,7 @@ 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 | ||||||
| @ -286,7 +289,7 @@ var ( | |||||||
| 	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{ | ||||||
| @ -351,8 +354,7 @@ var ( | |||||||
| 		urlTarget:  "https://example.com/nopenope.txt", | 		urlTarget:  "https://example.com/nopenope.txt", | ||||||
| 		wantResult: false, | 		wantResult: false, | ||||||
| 	}, | 	}, | ||||||
| 	} | } | ||||||
| ) |  | ||||||
| 
 | 
 | ||||||
| func TestMatchExpressionMatch(t *testing.T) { | func TestMatchExpressionMatch(t *testing.T) { | ||||||
| 	for _, tst := range expressionTests { | 	for _, tst := range expressionTests { | ||||||
|  | |||||||
| @ -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) |  | ||||||
| ) | ) | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
| @ -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) { | ||||||
|  | |||||||
| @ -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) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -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" | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user