diff --git a/lib/AOT/runtime/superglobals_refresh.c b/lib/AOT/runtime/superglobals_refresh.c index aaa513229..d3591d5ab 100644 --- a/lib/AOT/runtime/superglobals_refresh.c +++ b/lib/AOT/runtime/superglobals_refresh.c @@ -7,6 +7,14 @@ #include #include +#if defined(__APPLE__) || defined(__FreeBSD__) +#include +#define phpc_environ (*_NSGetEnviron()) +#else +extern char **environ; +#define phpc_environ environ +#endif + typedef struct __hashtable__ __hashtable__; typedef struct __string__ __string__; @@ -86,6 +94,40 @@ static const char *request_method_for(const char *post_body) return ('\0' != post_body[0]) ? "POST" : "GET"; } +static int is_cgi_header_env_key(const char *key) +{ + if (0 == strncmp(key, "HTTP_", 5)) { + return 1; + } + + return 0 == strcmp(key, "CONTENT_TYPE") || 0 == strcmp(key, "CONTENT_LENGTH"); +} + +static void apply_cgi_headers_from_environ(__hashtable__ *server) +{ + char **env; + char key_buf[256]; + + for (env = phpc_environ; NULL != env && NULL != *env; env++) { + const char *eq = strchr(*env, '='); + const char *value; + + if (NULL == eq) { + continue; + } + if ((size_t) (eq - *env) >= sizeof(key_buf)) { + continue; + } + memcpy(key_buf, *env, (size_t) (eq - *env)); + key_buf[eq - *env] = '\0'; + if (!is_cgi_header_env_key(key_buf)) { + continue; + } + value = eq + 1; + set_string_key(server, key_buf, value); + } +} + static void derive_path_info(const char *script_name, const char *request_uri, char *out, size_t out_len) { char path_buf[1024]; @@ -172,6 +214,8 @@ void __superglobals__refresh(void) set_string_key(sg_SERVER, "PATH_INFO", path_info); } + apply_cgi_headers_from_environ(sg_SERVER); + if (NULL == sg_COOKIE) { sg_COOKIE = __hashtable__alloc(); } diff --git a/lib/Web/DevServer.php b/lib/Web/DevServer.php index 3f70fc820..18472231b 100644 --- a/lib/Web/DevServer.php +++ b/lib/Web/DevServer.php @@ -122,11 +122,7 @@ public static function handleConnection($conn, string $docroot, callable $handle } self::clearHttpServerKeys(); - $httpServer = self::httpHeadersToServerVars($headers); - foreach ($httpServer as $key => $value) { - $_SERVER[$key] = $value; - } - $cgiEnv = array_merge($cgiEnv, $httpServer); + $cgiEnv = array_merge($cgiEnv, Superglobals::applyHttpHeaders($headers)); putenv('REQUEST_METHOD='.$method); putenv('QUERY_STRING='.$query); @@ -205,7 +201,7 @@ public static function readRequest($conn): ?array */ public static function headerNameToServerKey(string $name): string { - return 'HTTP_'.strtoupper(str_replace('-', '_', $name)); + return Superglobals::headerNameToServerKey($name); } /** diff --git a/lib/Web/Superglobals.php b/lib/Web/Superglobals.php index 670b0aa1a..528fd5bf1 100644 --- a/lib/Web/Superglobals.php +++ b/lib/Web/Superglobals.php @@ -22,6 +22,12 @@ final class Superglobals { private static ?Context $activeContext = null; + /** Maximum incoming request headers mapped into $_SERVER (issue #77). */ + public const MAX_HTTP_HEADERS = 64; + + /** Maximum length of a single header name or value after trimming. */ + public const MAX_HTTP_HEADER_LENGTH = 8192; + public const NAMES = [ '_GET', '_POST', @@ -59,6 +65,54 @@ public static function populateFromEnvironment( self::populateRequest($context); } + /** + * Map an HTTP header name to a CGI-style $_SERVER key (HTTP_* or CONTENT_*). + */ + public static function headerNameToServerKey(string $name): string + { + $normalized = strtoupper(str_replace('-', '_', trim($name))); + if ('CONTENT_TYPE' === $normalized || 'CONTENT_LENGTH' === $normalized) { + return $normalized; + } + + return 'HTTP_'.$normalized; + } + + /** + * Apply parsed request headers to PHP $_SERVER and putenv for CGI/AOT refresh. + * + * @param array $headers lowercase header name => value + * + * @return array CGI env entries (HTTP_* / CONTENT_*) + */ + public static function applyHttpHeaders(array $headers): array + { + $cgi = []; + $count = 0; + foreach ($headers as $name => $value) { + if ($count >= self::MAX_HTTP_HEADERS) { + break; + } + if (!is_string($name) || !is_string($value)) { + continue; + } + $name = trim($name); + $value = trim($value); + if ('' === $name || strlen($name) > self::MAX_HTTP_HEADER_LENGTH + || strlen($value) > self::MAX_HTTP_HEADER_LENGTH + || str_contains($value, "\r") || str_contains($value, "\n")) { + continue; + } + $key = self::headerNameToServerKey($name); + $_SERVER[$key] = $value; + putenv($key.'='.$value); + $cgi[$key] = $value; + ++$count; + } + + return $cgi; + } + private static function populateGet(Context $context, string $queryString): void { $get = $context->ensureSuperglobal('_GET'); @@ -113,11 +167,11 @@ private static function populateServer( self::setStringEntry($server, 'GATEWAY_INTERFACE', 'CGI/1.1'); self::setStringEntry($server, 'SERVER_SOFTWARE', 'PHP-Compiler-VM'); - foreach ($_SERVER as $key => $value) { + foreach (array_merge($_ENV, $_SERVER) as $key => $value) { if (!is_string($key) || !is_string($value)) { continue; } - if (str_starts_with($key, 'HTTP_')) { + if (str_starts_with($key, 'HTTP_') || str_starts_with($key, 'CONTENT_')) { self::setStringEntry($server, $key, $value); } } diff --git a/test/aot/RuntimeSuperglobalRefreshTest.php b/test/aot/RuntimeSuperglobalRefreshTest.php index db8547aba..d7047a3ad 100644 --- a/test/aot/RuntimeSuperglobalRefreshTest.php +++ b/test/aot/RuntimeSuperglobalRefreshTest.php @@ -78,6 +78,57 @@ public function testTwoRequestsDifferentQueryString(): void @unlink($outfile); } + public function testHttpHostFromCgiEnvironment(): void + { + $source = <<<'PHP' +assertNotFalse($outfile); + unlink($outfile); + + $repoRoot = dirname(__DIR__, 2); + $env = $this->llvmProcessEnv($repoRoot); + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $compile = proc_open( + array_merge( + self::llvmEnvPrefix(), + self::phpCommand(), + [$this->compileBin, '-o', $outfile] + ), + $descriptorSpec, + $pipes, + $repoRoot, + $env + ); + fwrite($pipes[0], $source); + fclose($pipes[0]); + $compileErr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($compile); + $this->assertFileExists($outfile, trim($compileErr !== false ? $compileErr : '')); + + $runEnv = $env; + $runEnv['HTTP_HOST'] = 'example.test'; + $runEnv['HTTP_X_CUSTOM'] = '1'; + $runEnv['SCRIPT_NAME'] = '/index.php'; + $runEnv['REQUEST_URI'] = '/index.php'; + $output = $this->runBinary($outfile, $runEnv); + $this->assertStringContainsString('example.test1', $output); + + @unlink($outfile); + } + /** * @param array $env */ diff --git a/test/unit/Web/SuperglobalsHttpHeadersTest.php b/test/unit/Web/SuperglobalsHttpHeadersTest.php new file mode 100644 index 000000000..8f0c85f42 --- /dev/null +++ b/test/unit/Web/SuperglobalsHttpHeadersTest.php @@ -0,0 +1,47 @@ +assertSame('HTTP_HOST', Superglobals::headerNameToServerKey('Host')); + $this->assertSame('HTTP_X_CUSTOM', Superglobals::headerNameToServerKey('X-Custom')); + $this->assertSame('CONTENT_TYPE', Superglobals::headerNameToServerKey('Content-Type')); + $this->assertSame('CONTENT_LENGTH', Superglobals::headerNameToServerKey('content-length')); + } + + public function testApplyHttpHeadersSetsCgiEnvironment(): void + { + $cgi = Superglobals::applyHttpHeaders([ + 'host' => 'example.test', + 'x-custom' => '1', + ]); + + $this->assertSame([ + 'HTTP_HOST' => 'example.test', + 'HTTP_X_CUSTOM' => '1', + ], $cgi); + $this->assertSame('example.test', $_SERVER['HTTP_HOST'] ?? ''); + $this->assertSame('1', getenv('HTTP_X_CUSTOM')); + } +}