mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 00:52:11 +01:00
Handle symlinking edge cases (#1660)
This one is interesting — though I’m not sure the best way to provide a test. I will have to look into maybe an integration test because it is a careful dance between how we resolve paths in the Caddy module vs. workers. I looked into making a proper change (literally using the same logic everywhere), but I think it is best to wait until #1646 is merged. But anyway, this change deals with some interesting edge cases. I will use gherkin to describe them: ```gherkin Feature: FrankenPHP symlinked edge cases Background: Given a `test` folder And a `public` folder linked to `test` And a worker script located at `test/index.php` And a `test/nested` folder And a worker script located at `test/nested/index.php` 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 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 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 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 ``` Trying to write that out in regular English would be more complex IMHO. These scenarios should all pass now with this PR. --------- Signed-off-by: Marc <m@pyc.ac> Co-authored-by: henderkes <m@pyc.ac> Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
This commit is contained in:
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
1
testdata/symlinks/public
vendored
Symbolic link
1
testdata/symlinks/public
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
test
|
||||
13
testdata/symlinks/test/document-root.php
vendored
Normal file
13
testdata/symlinks/test/document-root.php
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
if (isset($_SERVER['FRANKENPHP_WORKER'])) {
|
||||
$i = 0;
|
||||
do {
|
||||
$ok = frankenphp_handle_request(function () use ($i): void {
|
||||
echo "DOCUMENT_ROOT=" . $_SERVER['DOCUMENT_ROOT'] . "\n";
|
||||
});
|
||||
$i++;
|
||||
} while ($ok);
|
||||
} else {
|
||||
echo "DOCUMENT_ROOT=" . $_SERVER['DOCUMENT_ROOT'] . "\n";
|
||||
}
|
||||
13
testdata/symlinks/test/index.php
vendored
Normal file
13
testdata/symlinks/test/index.php
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
if (!isset($_SERVER['FRANKENPHP_WORKER'])) {
|
||||
die("Error: This script must be run in worker mode (FRANKENPHP_WORKER not set to '1')\n");
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
do {
|
||||
$ok = frankenphp_handle_request(function () use ($i): void {
|
||||
echo sprintf("Request: %d\n", $i);
|
||||
});
|
||||
$i++;
|
||||
} while ($ok);
|
||||
13
testdata/symlinks/test/nested/index.php
vendored
Normal file
13
testdata/symlinks/test/nested/index.php
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
if (!isset($_SERVER['FRANKENPHP_WORKER'])) {
|
||||
die("Error: This script must be run in worker mode (FRANKENPHP_WORKER not set to '1')\n");
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
do {
|
||||
$ok = frankenphp_handle_request(function () use ($i): void {
|
||||
echo sprintf("Nested request: %d\n", $i);
|
||||
});
|
||||
$i++;
|
||||
} while ($ok);
|
||||
@@ -111,7 +111,14 @@ func getWorkerByPath(path string) *worker {
|
||||
}
|
||||
|
||||
func newWorker(o workerOpt) (*worker, error) {
|
||||
absFileName, err := fastabs.FastAbs(o.fileName)
|
||||
// Order is important!
|
||||
// This order ensures that FrankenPHP started from inside a symlinked directory will properly resolve any paths.
|
||||
// If it is started from outside a symlinked directory, it is resolved to the same path that we use in the Caddy module.
|
||||
absFileName, err := filepath.EvalSymlinks(o.fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("worker filename is invalid %q: %w", o.fileName, err)
|
||||
}
|
||||
absFileName, err = fastabs.FastAbs(absFileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("worker filename is invalid %q: %w", o.fileName, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user