Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions lib/AOT/runtime/superglobals_refresh.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
#include <stdlib.h>
#include <string.h>

#if defined(__APPLE__) || defined(__FreeBSD__)
#include <crt_externs.h>
#define phpc_environ (*_NSGetEnviron())
#else
extern char **environ;
#define phpc_environ environ
#endif

typedef struct __hashtable__ __hashtable__;
typedef struct __string__ __string__;

Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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();
}
Expand Down
8 changes: 2 additions & 6 deletions lib/Web/DevServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

/**
Expand Down
58 changes: 56 additions & 2 deletions lib/Web/Superglobals.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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<string, string> $headers lowercase header name => value
*
* @return array<string, string> 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');
Expand Down Expand Up @@ -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);
}
}
Expand Down
51 changes: 51 additions & 0 deletions test/aot/RuntimeSuperglobalRefreshTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,57 @@ public function testTwoRequestsDifferentQueryString(): void
@unlink($outfile);
}

public function testHttpHostFromCgiEnvironment(): void
{
$source = <<<'PHP'
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
echo $_SERVER['HTTP_HOST'], $_SERVER['HTTP_X_CUSTOM'];
PHP;

$outfile = tempnam(sys_get_temp_dir(), 'phpc_http_hdr_');
$this->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<string, string> $env
*/
Expand Down
47 changes: 47 additions & 0 deletions test/unit/Web/SuperglobalsHttpHeadersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

use PHPCompiler\Web\Superglobals;
use PHPUnit\Framework\TestCase;

/**
* Issue #193: HTTP request headers mapped to $_SERVER HTTP_* keys.
*/
final class SuperglobalsHttpHeadersTest extends TestCase
{
protected function tearDown(): void
{
foreach (array_keys($_SERVER) as $key) {
if (str_starts_with($key, 'HTTP_') || str_starts_with($key, 'CONTENT_')) {
unset($_SERVER[$key]);
putenv($key);
}
}
}

public function testHeaderNameToServerKey(): void
{
$this->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'));
}
}
Loading