diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go index e359e3d4bc..4233fc3111 100644 --- a/caddy/caddy_test.go +++ b/caddy/caddy_test.go @@ -1500,3 +1500,253 @@ func TestLog(t *testing.T) { "", ) } + +// TestSymlinkWorkerPaths tests different ways to reference worker scripts in symlinked directories +func TestSymlinkWorkerPaths(t *testing.T) { + cwd, _ := os.Getwd() + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + t.Run("NeighboringWorkerScript", func(t *testing.T) { + // 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 + 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") + }) + + t.Run("NestedWorkerScript", func(t *testing.T) { + // 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 + 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") + }) + + t.Run("OutsideSymlinkedFolder", func(t *testing.T) { + // 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 + 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") + }) + + t.Run("SpecifiedRootDirectory", func(t *testing.T) { + // 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 + 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") + }) +} + +// TestSymlinkResolveRoot tests the resolve_root_symlink directive behavior +func TestSymlinkResolveRoot(t *testing.T) { + cwd, _ := os.Getwd() + testDir := filepath.Join(cwd, "..", "testdata", "symlinks", "test") + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + t.Run("ResolveRootSymlink", func(t *testing.T) { + // Tests that resolve_root_symlink directive works correctly + 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") + }) + + t.Run("NoResolveRootSymlink", func(t *testing.T) { + // Tests that symlinks are preserved when resolve_root_symlink is false (non-worker mode) + 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") + }) +} + +// TestSymlinkWorkerBehavior tests worker behavior with symlinked directories +func TestSymlinkWorkerBehavior(t *testing.T) { + cwd, _ := os.Getwd() + publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public") + + t.Run("WorkerScriptFailsWithoutWorkerMode", func(t *testing.T) { + // Tests that accessing a worker-only script without configuring it as a worker actually results in an error + 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") + }) + + t.Run("MultipleRequests", func(t *testing.T) { + // Tests that symlinked workers handle multiple requests correctly + 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 @@ +