diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go index e359e3d4bc..15a0562cc3 100644 --- a/caddy/caddy_test.go +++ b/caddy/caddy_test.go @@ -1500,3 +1500,269 @@ func TestLog(t *testing.T) { "", ) } + +// TestSymlinkNeighboringWorkerScript tests executing a worker script from within a symlinked directory +// Scenario: neighboring worker script +// +// Given frankenphp located in the test folder +// When I execute `frankenphp php-server --listen localhost:8080 -w index.php` from `public` +// Then I expect to see the worker script executed successfully +func TestSymlinkNeighboringWorkerScript(t *testing.T) { + cwd, _ := os.Getwd() + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port `+testPort+` + + frankenphp { + worker `+publicDir+`/index.php 1 + } + } + + localhost:`+testPort+` { + route { + php { + root `+publicDir+` + resolve_root_symlink true + } + } + } + `, "caddyfile") + + tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, "Request: 0\n") +} + +// TestSymlinkNestedWorkerScript tests executing a nested worker script through a symlinked directory +// Scenario: nested worker script +// +// Given frankenphp located in the test folder +// When I execute `frankenphp --listen localhost:8080 -w nested/index.php` from `public` +// Then I expect to see the worker script executed successfully +func TestSymlinkNestedWorkerScript(t *testing.T) { + cwd, _ := os.Getwd() + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port `+testPort+` + + frankenphp { + worker `+publicDir+`/nested/index.php 1 + } + } + + localhost:`+testPort+` { + route { + php { + root `+publicDir+` + resolve_root_symlink true + } + } + } + `, "caddyfile") + + tester.AssertGetResponse("http://localhost:"+testPort+"/nested/index.php", http.StatusOK, "Nested request: 0\n") +} + +// TestSymlinkOutsideSymlinkedFolder tests executing a worker script referenced from outside the symlinked directory +// Scenario: outside the symlinked folder +// +// Given frankenphp located in the root folder +// When I execute `frankenphp --listen localhost:8080 -w public/index.php` from the root folder +// Then I expect to see the worker script executed successfully +func TestSymlinkOutsideSymlinkedFolder(t *testing.T) { + cwd, _ := os.Getwd() + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port `+testPort+` + + frankenphp { + worker { + name outside_worker + file `+publicDir+`/index.php + num 1 + } + } + } + + localhost:`+testPort+` { + route { + php { + root `+publicDir+` + resolve_root_symlink true + } + } + } + `, "caddyfile") + + tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, "Request: 0\n") +} + +// TestSymlinkSpecifiedRootDirectory tests executing a worker script with an explicitly specified root directory +// Scenario: specified root directory +// +// Given frankenphp located in the root folder +// When I execute `frankenphp --listen localhost:8080 -w public/index.php -r public` from the root folder +// Then I expect to see the worker script executed successfully +func TestSymlinkSpecifiedRootDirectory(t *testing.T) { + cwd, _ := os.Getwd() + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port `+testPort+` + + frankenphp { + worker { + name specified_root_worker + file `+publicDir+`/index.php + num 1 + } + } + } + + localhost:`+testPort+` { + route { + php { + root `+publicDir+` + resolve_root_symlink true + } + } + } + `, "caddyfile") + + tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, "Request: 0\n") +} + +// TestSymlinkResolveRootSymlink tests that resolve_root_symlink directive works correctly +func TestSymlinkResolveRootSymlink(t *testing.T) { + cwd, _ := os.Getwd() + testDir := filepath.Join(cwd, "..", "testdata", "symlinks", "test") + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port `+testPort+` + + frankenphp { + worker `+publicDir+`/document-root.php 1 + } + } + + localhost:`+testPort+` { + route { + php { + root `+publicDir+` + resolve_root_symlink true + } + } + } + `, "caddyfile") + + // DOCUMENT_ROOT should be the resolved path (testDir) + tester.AssertGetResponse("http://localhost:"+testPort+"/document-root.php", http.StatusOK, "DOCUMENT_ROOT="+testDir+"\n") +} + +// TestSymlinkNoResolveRootSymlink tests that symlinks are preserved when resolve_root_symlink is false (non-worker mode) +// Note: This test uses document-root.php in non-worker mode to verify DOCUMENT_ROOT contains the symlink path +func TestSymlinkNoResolveRootSymlink(t *testing.T) { + cwd, _ := os.Getwd() + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port `+testPort+` + } + + localhost:`+testPort+` { + route { + php { + root `+publicDir+` + resolve_root_symlink false + } + } + } + `, "caddyfile") + + // DOCUMENT_ROOT should be the symlink path (publicDir) when resolve_root_symlink is false + tester.AssertGetResponse("http://localhost:"+testPort+"/document-root.php", http.StatusOK, "DOCUMENT_ROOT="+publicDir+"\n") +} + +// TestSymlinkWorkerScriptFailsWithoutWorkerMode tests that accessing a worker script +// without configuring it as a worker actually results in an error +func TestSymlinkWorkerScriptFailsWithoutWorkerMode(t *testing.T) { + cwd, _ := os.Getwd() + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port `+testPort+` + } + + localhost:`+testPort+` { + route { + php { + root `+publicDir+` + } + } + } + `, "caddyfile") + + // Accessing the worker script without worker configuration MUST fail + // The script checks $_SERVER['FRANKENPHP_WORKER'] and dies if not set + tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, "Error: This script must be run in worker mode (FRANKENPHP_WORKER not set to '1')\n") +} + +// TestSymlinkMultipleRequests tests that symlinked workers handle multiple sequential requests correctly +func TestSymlinkMultipleRequests(t *testing.T) { + cwd, _ := os.Getwd() + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port `+testPort+` + } + + localhost:`+testPort+` { + route { + php { + root `+publicDir+` + resolve_root_symlink true + worker index.php 1 + } + } + } + `, "caddyfile") + + // Make multiple requests - each should increment the counter + for i := 0; i < 5; i++ { + tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, fmt.Sprintf("Request: %d\n", i)) + } +} diff --git a/caddy/module.go b/caddy/module.go index b116a5e1ac..6416362694 100644 --- a/caddy/module.go +++ b/caddy/module.go @@ -138,6 +138,15 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error { } f.resolvedDocumentRoot = root + + // Also resolve symlinks in worker file paths when resolve_root_symlink is true + for i, wc := range f.Workers { + if !filepath.IsAbs(wc.FileName) { + continue + } + resolvedPath, _ := filepath.EvalSymlinks(wc.FileName) + f.Workers[i].FileName = resolvedPath + } } } @@ -181,7 +190,10 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c if documentRoot == "" && frankenphp.EmbeddedAppPath != "" { documentRoot = frankenphp.EmbeddedAppPath } - documentRootOption = frankenphp.WithRequestDocumentRoot(documentRoot, *f.ResolveRootSymlink) + // If we do not have a resolved document root, then we cannot resolve the symlink of our cwd because it may + // resolve to a different directory than the one we are currently in. + // This is especially important if there are workers running. + documentRootOption = frankenphp.WithRequestDocumentRoot(documentRoot, false) } else { documentRoot = f.resolvedDocumentRoot documentRootOption = frankenphp.WithRequestResolvedDocumentRoot(documentRoot) diff --git a/internal/testext/exttest.go b/internal/testext/exttest.go index abebee4c1d..1a8477d4a8 100644 --- a/internal/testext/exttest.go +++ b/internal/testext/exttest.go @@ -11,13 +11,14 @@ package testext // #include "extension.h" import "C" import ( - "github.com/dunglas/frankenphp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "io" "net/http/httptest" "testing" "unsafe" + + "github.com/dunglas/frankenphp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func testRegisterExtension(t *testing.T) { diff --git a/testdata/symlinks/public b/testdata/symlinks/public new file mode 120000 index 0000000000..30d74d2584 --- /dev/null +++ b/testdata/symlinks/public @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/testdata/symlinks/test/document-root.php b/testdata/symlinks/test/document-root.php new file mode 100644 index 0000000000..c21b2fc7fc --- /dev/null +++ b/testdata/symlinks/test/document-root.php @@ -0,0 +1,13 @@ +