mirror of
https://github.com/dunglas/frankenphp.git
synced 2026-04-22 16:27:12 +08:00
fix: set $_SERVER variables: 'SCRIPT_NAME', 'PHP_SELF', and 'PATH_INFO' (#2317)
fixes https://github.com/php/frankenphp/issues/2274#issuecomment-4142767490 closes https://github.com/php/frankenphp/issues/1133 apache/nginx/caddy pass PHP_SELF as SCRIPT_NAME + PATH_INFO, but our PATH_INFO wasn't working because our matcher stripped the rest of the path. request url: localhost/index.php/en ``` # was non-worker: SCRIPT_NAME: /index.php PATH_INFO: PHP_SELF: /index.php REQUEST_URL: /en # was fastcgi: SCRIPT_NAME: /index.php PATH_INFO: /en PHP_SELF: /index.php/en REQUEST_URL: /en # was php_server worker SCRIPT_NAME: PATH_INFO: PHP_SELF: /en REQUEST_URL: /en # now is always: SCRIPT_NAME: /index.php PATH_INFO: /en PHP_SELF: /index.php/en REQUEST_URL: /en ``` --------- Signed-off-by: Marc <m@pyc.ac> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
/caddy/frankenphp/Build
|
||||
/caddy/frankenphp/Caddyfile.test
|
||||
/caddy/frankenphp/frankenphp
|
||||
/caddy/frankenphp/frankenphp.exe
|
||||
/caddy/frankenphp/public
|
||||
/dist
|
||||
/github_conf
|
||||
/internal/testserver/testserver
|
||||
|
||||
@@ -486,6 +486,195 @@ func TestPHPServerDirectiveDisableFileServer(t *testing.T) {
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/not-found.txt", http.StatusOK, "I am by birth a Genevese (i not set)")
|
||||
}
|
||||
|
||||
func TestPHPServerGlobals(t *testing.T) {
|
||||
documentRoot, _ := filepath.Abs("../testdata")
|
||||
scriptFilename := filepath.Join(documentRoot, "server-globals.php")
|
||||
|
||||
tester := caddytest.NewTester(t)
|
||||
initServer(t, tester, `
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port `+testPort+`
|
||||
https_port 9443
|
||||
}
|
||||
|
||||
localhost:`+testPort+` {
|
||||
root ../testdata
|
||||
php_server {
|
||||
index server-globals.php
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
// Request to /en: no matching file, falls through to server-globals.php worker
|
||||
// SCRIPT_NAME should be /server-globals.php, PHP_SELF should be /server-globals.php (no /en), PATH_INFO empty
|
||||
tester.AssertGetResponse(
|
||||
"http://localhost:"+testPort+"/en",
|
||||
http.StatusOK,
|
||||
fmt.Sprintf(`SCRIPT_NAME: /server-globals.php
|
||||
SCRIPT_FILENAME: %s
|
||||
PHP_SELF: /server-globals.php
|
||||
PATH_INFO:
|
||||
DOCUMENT_ROOT: %s
|
||||
DOCUMENT_URI: /server-globals.php
|
||||
REQUEST_URI: /en
|
||||
`, scriptFilename, documentRoot),
|
||||
)
|
||||
|
||||
// Request to /server-globals.php/en: explicit PHP file with path info
|
||||
// SCRIPT_NAME should be /server-globals.php, PHP_SELF should be /server-globals.php/en, PATH_INFO should be /en
|
||||
tester.AssertGetResponse(
|
||||
"http://localhost:"+testPort+"/server-globals.php/en",
|
||||
http.StatusOK,
|
||||
fmt.Sprintf(`SCRIPT_NAME: /server-globals.php
|
||||
SCRIPT_FILENAME: %s
|
||||
PHP_SELF: /server-globals.php/en
|
||||
PATH_INFO: /en
|
||||
DOCUMENT_ROOT: %s
|
||||
DOCUMENT_URI: /server-globals.php
|
||||
REQUEST_URI: /server-globals.php/en
|
||||
`, scriptFilename, documentRoot),
|
||||
)
|
||||
}
|
||||
|
||||
func TestWorkerPHPServerGlobals(t *testing.T) {
|
||||
documentRoot, _ := filepath.Abs("../testdata")
|
||||
documentRoot2, _ := filepath.Abs("../caddy")
|
||||
scriptFilename := documentRoot + string(filepath.Separator) + "server-globals.php"
|
||||
testPortNum, _ := strconv.Atoi(testPort)
|
||||
testPortTwo := strconv.Itoa(testPortNum + 1)
|
||||
testPortThree := strconv.Itoa(testPortNum + 2)
|
||||
|
||||
tester := caddytest.NewTester(t)
|
||||
initServer(t, tester, `
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
|
||||
frankenphp {
|
||||
worker {
|
||||
file ../testdata/server-globals.php
|
||||
num 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http://localhost:`+testPort+` {
|
||||
php_server {
|
||||
root ../testdata
|
||||
index server-globals.php
|
||||
}
|
||||
}
|
||||
|
||||
http://localhost:`+testPortTwo+` {
|
||||
php_server {
|
||||
root ../testdata
|
||||
index server-globals.php
|
||||
worker {
|
||||
file server-globals.php
|
||||
num 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http://localhost:`+testPortThree+` {
|
||||
php_server {
|
||||
root ./
|
||||
index server-globals.php
|
||||
worker {
|
||||
file ../testdata/server-globals.php
|
||||
num 1
|
||||
match *
|
||||
}
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
// === Site 1: global worker with php_server ===
|
||||
// because we don't specify a php file, PATH_INFO should be empty
|
||||
tester.AssertGetResponse(
|
||||
"http://localhost:"+testPort+"/en",
|
||||
http.StatusOK,
|
||||
fmt.Sprintf(`SCRIPT_NAME: /server-globals.php
|
||||
SCRIPT_FILENAME: %s
|
||||
PHP_SELF: /server-globals.php
|
||||
PATH_INFO:
|
||||
DOCUMENT_ROOT: %s
|
||||
DOCUMENT_URI: /server-globals.php
|
||||
REQUEST_URI: /en
|
||||
`, scriptFilename, documentRoot),
|
||||
)
|
||||
|
||||
tester.AssertGetResponse(
|
||||
"http://localhost:"+testPort+"/server-globals.php/en",
|
||||
http.StatusOK,
|
||||
fmt.Sprintf(`SCRIPT_NAME: /server-globals.php
|
||||
SCRIPT_FILENAME: %s
|
||||
PHP_SELF: /server-globals.php/en
|
||||
PATH_INFO: /en
|
||||
DOCUMENT_ROOT: %s
|
||||
DOCUMENT_URI: /server-globals.php
|
||||
REQUEST_URI: /server-globals.php/en
|
||||
`, scriptFilename, documentRoot),
|
||||
)
|
||||
|
||||
// === Site 2: php_server with its own worker ===
|
||||
// because the request does not specify a php file, PATH_INFO should be empty
|
||||
tester.AssertGetResponse(
|
||||
"http://localhost:"+testPortTwo+"/en",
|
||||
http.StatusOK,
|
||||
fmt.Sprintf(`SCRIPT_NAME: /server-globals.php
|
||||
SCRIPT_FILENAME: %s
|
||||
PHP_SELF: /server-globals.php
|
||||
PATH_INFO:
|
||||
DOCUMENT_ROOT: %s
|
||||
DOCUMENT_URI: /server-globals.php
|
||||
REQUEST_URI: /en
|
||||
`, scriptFilename, documentRoot),
|
||||
)
|
||||
|
||||
tester.AssertGetResponse(
|
||||
"http://localhost:"+testPortTwo+"/server-globals.php/en",
|
||||
http.StatusOK,
|
||||
fmt.Sprintf(`SCRIPT_NAME: /server-globals.php
|
||||
SCRIPT_FILENAME: %s
|
||||
PHP_SELF: /server-globals.php/en
|
||||
PATH_INFO: /en
|
||||
DOCUMENT_ROOT: %s
|
||||
DOCUMENT_URI: /server-globals.php
|
||||
REQUEST_URI: /server-globals.php/en
|
||||
`, scriptFilename, documentRoot),
|
||||
)
|
||||
|
||||
// === Site 3: php_server with its own match worker ===
|
||||
tester.AssertGetResponse(
|
||||
"http://localhost:"+testPortThree+"/en",
|
||||
http.StatusOK,
|
||||
fmt.Sprintf(`SCRIPT_NAME:
|
||||
SCRIPT_FILENAME: %s
|
||||
PHP_SELF:
|
||||
PATH_INFO:
|
||||
DOCUMENT_ROOT: %s
|
||||
DOCUMENT_URI:
|
||||
REQUEST_URI: /en
|
||||
`, scriptFilename, documentRoot2),
|
||||
)
|
||||
|
||||
tester.AssertGetResponse(
|
||||
"http://localhost:"+testPortThree+"/server-globals.php/en",
|
||||
http.StatusOK,
|
||||
fmt.Sprintf(`SCRIPT_NAME:
|
||||
SCRIPT_FILENAME: %s
|
||||
PHP_SELF:
|
||||
PATH_INFO:
|
||||
DOCUMENT_ROOT: %s
|
||||
DOCUMENT_URI:
|
||||
REQUEST_URI: /server-globals.php/en
|
||||
`, scriptFilename, documentRoot2),
|
||||
)
|
||||
}
|
||||
|
||||
func TestMetrics(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
tester := caddytest.NewTester(t)
|
||||
|
||||
+2
-2
@@ -559,7 +559,7 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
|
||||
}),
|
||||
}
|
||||
rewriteHandler := rewrite.Rewrite{
|
||||
URI: "{http.matchers.file.relative}",
|
||||
URI: "{http.matchers.file.relative}{http.matchers.file.remainder}",
|
||||
}
|
||||
rewriteRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
|
||||
@@ -573,7 +573,7 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
|
||||
// match only requests that are for PHP files
|
||||
var pathList []string
|
||||
for _, ext := range extensions {
|
||||
pathList = append(pathList, "*"+ext)
|
||||
pathList = append(pathList, "*"+ext, "*"+ext+"/*")
|
||||
}
|
||||
phpMatcherSet := caddy.ModuleMap{
|
||||
"path": h.JSON(pathList),
|
||||
|
||||
+2
-2
@@ -171,7 +171,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
|
||||
}, nil),
|
||||
}
|
||||
rewriteHandler := rewrite.Rewrite{
|
||||
URI: "{http.matchers.file.relative}",
|
||||
URI: "{http.matchers.file.relative}{http.matchers.file.remainder}",
|
||||
}
|
||||
rewriteRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
|
||||
@@ -182,7 +182,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
|
||||
// match only requests that are for PHP files
|
||||
var pathList []string
|
||||
for _, ext := range extensions {
|
||||
pathList = append(pathList, "*"+ext)
|
||||
pathList = append(pathList, "*"+ext, "*"+ext+"/*")
|
||||
}
|
||||
phpMatcherSet := caddy.ModuleMap{
|
||||
"path": caddyconfig.JSON(pathList, nil),
|
||||
|
||||
@@ -111,7 +111,7 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
|
||||
requestURI = fc.requestURI
|
||||
}
|
||||
|
||||
requestPath := ensureLeadingSlash(request.URL.Path)
|
||||
phpSelf := fc.scriptName + fc.pathInfo
|
||||
|
||||
C.frankenphp_register_server_vars(trackVarsArray, C.frankenphp_server_vars{
|
||||
// approximate total length to avoid array re-hashing:
|
||||
@@ -129,8 +129,8 @@ func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
|
||||
document_root_len: C.size_t(len(fc.documentRoot)),
|
||||
path_info: toUnsafeChar(fc.pathInfo),
|
||||
path_info_len: C.size_t(len(fc.pathInfo)),
|
||||
php_self: toUnsafeChar(requestPath),
|
||||
php_self_len: C.size_t(len(requestPath)),
|
||||
php_self: toUnsafeChar(phpSelf),
|
||||
php_self_len: C.size_t(len(phpSelf)),
|
||||
document_uri: toUnsafeChar(fc.docURI),
|
||||
document_uri_len: C.size_t(len(fc.docURI)),
|
||||
script_filename: toUnsafeChar(fc.scriptFilename),
|
||||
@@ -208,17 +208,26 @@ func splitCgiPath(fc *frankenPHPContext) {
|
||||
if splitPos := splitPos(path, splitPath); splitPos > -1 {
|
||||
fc.docURI = path[:splitPos]
|
||||
fc.pathInfo = path[splitPos:]
|
||||
|
||||
// Strip PATH_INFO from SCRIPT_NAME
|
||||
fc.scriptName = strings.TrimSuffix(path, fc.pathInfo)
|
||||
|
||||
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
|
||||
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
|
||||
if fc.scriptName != "" && !strings.HasPrefix(fc.scriptName, "/") {
|
||||
fc.scriptName = "/" + fc.scriptName
|
||||
}
|
||||
}
|
||||
|
||||
// If a worker is already assigned explicitly, derive SCRIPT_NAME from its filename
|
||||
if fc.worker != nil {
|
||||
fc.scriptFilename = fc.worker.fileName
|
||||
docRootWithSep := fc.documentRoot + string(filepath.Separator)
|
||||
if strings.HasPrefix(fc.worker.fileName, docRootWithSep) {
|
||||
fc.scriptName = filepath.ToSlash(strings.TrimPrefix(fc.worker.fileName, fc.documentRoot))
|
||||
} else {
|
||||
fc.docURI = ""
|
||||
fc.pathInfo = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Strip PATH_INFO from SCRIPT_NAME
|
||||
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
|
||||
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
|
||||
fc.scriptName = ensureLeadingSlash(strings.TrimSuffix(path, fc.pathInfo))
|
||||
|
||||
// TODO: is it possible to delay this and avoid saving everything in the context?
|
||||
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
|
||||
|
||||
+1
-8
@@ -86,14 +86,7 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
// If a worker is already assigned explicitly, use its filename and skip parsing path variables
|
||||
if fc.worker != nil {
|
||||
fc.scriptFilename = fc.worker.fileName
|
||||
} else {
|
||||
// If no worker was assigned, split the path into the "traditional" CGI path variables.
|
||||
// This needs to already happen here in case a worker script still matches the path.
|
||||
splitCgiPath(fc)
|
||||
}
|
||||
splitCgiPath(fc)
|
||||
|
||||
fc.requestURI = r.URL.RequestURI()
|
||||
|
||||
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
ignore_user_abort(true);
|
||||
|
||||
$server_vars = [
|
||||
'SCRIPT_NAME',
|
||||
'SCRIPT_FILENAME',
|
||||
'PHP_SELF',
|
||||
'PATH_INFO',
|
||||
'DOCUMENT_ROOT',
|
||||
'DOCUMENT_URI',
|
||||
'REQUEST_URI',
|
||||
];
|
||||
$handler = static function() use ($server_vars) {
|
||||
foreach ($server_vars as $var) {
|
||||
$value = $_SERVER[$var] ?? '(not set)';
|
||||
echo $value !== '' ? "$var: $value\n" : "$var:\n";
|
||||
}
|
||||
};
|
||||
|
||||
if (isset($_SERVER['FRANKENPHP_WORKER'])) {
|
||||
for ($nbRequests = 0, $running = true; $running; ++$nbRequests) {
|
||||
$running = \frankenphp_handle_request($handler);
|
||||
gc_collect_cycles();
|
||||
}
|
||||
} else {
|
||||
$handler();
|
||||
}
|
||||
Reference in New Issue
Block a user