fix: set $_SERVER variables: 'SCRIPT_NAME', 'PHP_SELF', and 'PATH_INFO' #2317
fix: set $_SERVER variables: 'SCRIPT_NAME', 'PHP_SELF', and 'PATH_INFO' #2317
Conversation
…rkers apache passes PHP_SELF as SCRIPT_NAME + REQUEST_URL, but the docs say it's the same as SCRIPT_NAME and that's how Caddy+fpm behave too
cgi.go
Outdated
| // If a worker is already assigned explicitly, derive SCRIPT_NAME from its filename | ||
| if fc.worker != nil { | ||
| fc.scriptFilename = fc.worker.fileName | ||
| fc.scriptName = strings.TrimPrefix(fc.worker.fileName, fc.documentRoot) |
There was a problem hiding this comment.
Hmm probably would be better to leave scriptName empty if the worker is in the public path:
root /some/path
worker /other/path {
match *
}There was a problem hiding this comment.
I'm not sure about that. Shouldn't it show /other/path/index.php then?
There was a problem hiding this comment.
Not sure either TBH. In CGI spec it's the path relative to the root, so /other/path/index.php might be misleading.
There was a problem hiding this comment.
Technically that's a path relative to the root, haha.
I don't see a better solution. Emptying it out would be even more of a violation.
… would activate for the non-worker case too
cgi.go
Outdated
| // If a worker is already assigned explicitly, derive SCRIPT_NAME from its filename | ||
| if fc.worker != nil { | ||
| fc.scriptFilename = fc.worker.fileName | ||
| fc.scriptName = filepath.ToSlash(strings.TrimPrefix(fc.worker.fileName, fc.documentRoot)) |
There was a problem hiding this comment.
Can you also add a test for when the worker is outside of the public path (match)? Thinking about it again, scriptname should be empty since forwarding an absoule path when expecting a relative one is probably worse than forwarding nothing.
There was a problem hiding this comment.
Happy to add the test, but I'm not sure if I agree with passing nothing being the correct choice. We should probably agree on what the correct behaviour is first.
'SCRIPT_NAME'
Contains the current script's path. This is useful for pages which need to point to themselves. The FILE constant contains the full path and filename of the current (i.e. included) file.
If anything, "This is useful for pages which need to point to themselves." would mean that we should perhaps just give the current request uri. But on the other hand, "Contains the current script's path." would mean it should be the absolute path, rather than a uri.
There was a problem hiding this comment.
It's still expected to be a relative path, so I'd rather prevent potential redirects like this (and just leave it empty):
/path-the-user-visits -> /app/vendor/some-framework/worker.php
Realistically, most worker mode implementations probably just ignore SCRIPT_NAME.
There was a problem hiding this comment.
I don't know, that doesn't feel right to me. I've changed it to what you suggested either way. Nobody should be using these server vars anyway.
| @@ -208,17 +208,22 @@ func splitCgiPath(fc *frankenPHPContext) { | |||
| if splitPos := splitPos(path, splitPath); splitPos > -1 { | |||
| fc.docURI = path[:splitPos] | |||
| fc.pathInfo = path[splitPos:] | |||
There was a problem hiding this comment.
Hmm I think if a worker is explicitly assigned, we should also ensure that the .php file is the actual worker file before determining pathInfo.
There was a problem hiding this comment.
In case of a match *?
There was a problem hiding this comment.
Yeah, we probably shouldn't allow pathname from a random script name like:
/random-script.php/pathname
Probably best to just leave it empty if script_name is empty.
It's a bit confusing because in all other cases the script name is guaranteed to be there because of rewrites.
There was a problem hiding this comment.
went for a simple if check in order to not introduce runtime another check
it's probably very unlikely to to a different worker file in the public document root
Co-authored-by: Marc <[email protected]> Signed-off-by: Marc <[email protected]>
dunglas
left a comment
There was a problem hiding this comment.
LGTM, but I didn't carefully review the CGI spec nor existing Apache/FPM behaviors. I trust you on this
| run: go build | ||
| - name: Compile library tests | ||
| run: go test -race -v -x -c | ||
| run: go test -race -failfast -v -x -c |
There was a problem hiding this comment.
I would prefer continuing to run the full test suite to catch all errors in one error.
There was a problem hiding this comment.
Is there a way to see what actually failed without scrolling through 1000 lines? It irks me every time.
| ignore_user_abort(true); | ||
|
|
||
| $handler = static function() { | ||
| echo "SCRIPT_NAME: " . ($_SERVER['SCRIPT_NAME'] ?? '(not set)') . "<br>"; |
There was a problem hiding this comment.
Nit: you could ass \n and the end of each line, so you could use backticks for better readability in tests
| @@ -1,6 +1,8 @@ | |||
| /caddy/frankenphp/Build | |||
There was a problem hiding this comment.
added these because I often times find myself verifying bug reports there
I'm thinking they're fine in the .gitignore, but if you want I can revert
There was a problem hiding this comment.
Pull request overview
This PR fixes inconsistencies in $_SERVER CGI variables (notably SCRIPT_NAME, PHP_SELF, and PATH_INFO) in worker mode by ensuring path splitting happens consistently and by preserving “remainder” path segments through the Caddy rewrite/matching pipeline.
Changes:
- Always split CGI path variables during request context creation, including in worker mode.
- Set
PHP_SELFfromSCRIPT_NAME + PATH_INFO, and update CGI path splitting logic for worker-assigned requests. - Update Caddy
php_serverrewrite/match patterns to preserve remainder path info and add regression tests for server globals.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
context.go |
Always calls splitCgiPath(fc) to compute CGI path vars consistently. |
cgi.go |
Updates PHP_SELF derivation and refactors splitCgiPath to better support worker mode. |
caddy/php-server.go |
Preserves remainder in rewrite and matches *.php/* paths to support PATH_INFO. |
caddy/module.go |
Same Caddy rewrite/matcher adjustments for the directive parser output. |
caddy/caddy_test.go |
Adds coverage asserting expected globals for both worker and non-worker php_server setups. |
testdata/server-globals.php |
New test helper script that prints key $_SERVER variables. |
.github/workflows/tests.yaml |
Adds -failfast to speed up CI failure feedback. |
.gitignore |
Ignores additional generated Caddy test artifacts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| `, "caddyfile") | ||
|
|
||
| // Request to /en: no matching file, falls through to server-globals.php worker |
There was a problem hiding this comment.
This comment is misleading: there is no worker configured in this test, so the request to /en is handled by the php_server index fallback, not a worker. Updating the comment would avoid confusion when maintaining these globals expectations.
| // Request to /en: no matching file, falls through to server-globals.php worker | |
| // Request to /en: no matching file, so php_server falls back to the index script server-globals.php |
| ) | ||
|
|
||
| // === Site 2: php_server with its own worker === | ||
| // because we specify a php file, PATH_INFO should be /en |
There was a problem hiding this comment.
The comment says "PATH_INFO should be /en" for the /en request, but the assertion expects PATH_INFO to be empty. Either the comment or the expected output should be corrected so they agree.
| // because we specify a php file, PATH_INFO should be /en | |
| // because the request does not specify a php file, PATH_INFO should be empty |
|
|
||
| func TestPHPServerGlobals(t *testing.T) { | ||
| documentRoot, _ := filepath.Abs("../testdata") | ||
| scriptFilename := documentRoot + string(filepath.Separator) + "server-globals.php" |
There was a problem hiding this comment.
For portability/readability, build scriptFilename with filepath.Join(documentRoot, "server-globals.php") instead of string concatenation with filepath.Separator.
| scriptFilename := documentRoot + string(filepath.Separator) + "server-globals.php" | |
| scriptFilename := filepath.Join(documentRoot, "server-globals.php") |
fixes #2274 (comment)
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