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
116 changes: 116 additions & 0 deletions lib/AOT/runtime/superglobals_refresh.c
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,121 @@ static void apply_cgi_headers_from_environ(__hashtable__ *server)
}
}

static int sg_is_https_request(void)
{
const char *https = getenv("HTTPS");

if (NULL != https && '\0' != https[0] && 0 != strcmp(https, "0")
&& 0 != strcasecmp(https, "off")) {
return 1;
}
{
const char *proto = getenv("HTTP_X_FORWARDED_PROTO");

if (NULL != proto && 0 == strcasecmp(proto, "https")) {
return 1;
}
}

return 0;
}

static int sg_parse_host_port(const char *host, char *name_out, size_t name_len, int *port_out)
{
const char *colon;

name_out[0] = '\0';
*port_out = 0;
if ('\0' == host[0]) {
return 0;
}
if ('[' == host[0]) {
const char *close = strchr(host, ']');

if (NULL != close) {
size_t name_part = (size_t) (close - host - 1);

if (name_part >= name_len) {
name_part = name_len - 1;
}
memcpy(name_out, host + 1, name_part);
name_out[name_part] = '\0';
if (']' == close[0] && ':' == close[1]) {
*port_out = atoi(close + 2);
}

return 1;
}
}
colon = strrchr(host, ':');
if (NULL != colon && NULL == strchr(colon + 1, ':')) {
int port = atoi(colon + 1);

if (port > 0) {
size_t name_part = (size_t) (colon - host);

if (name_part >= name_len) {
name_part = name_len - 1;
}
memcpy(name_out, host, name_part);
name_out[name_part] = '\0';
*port_out = port;

return 1;
}
}
strncpy(name_out, host, name_len - 1);
name_out[name_len - 1] = '\0';

return 1;
}

static int sg_resolve_server_port(int https, int port_from_host)
{
const char *from_env = getenv("SERVER_PORT");

if (NULL != from_env && '\0' != from_env[0]) {
int port = atoi(from_env);

if (port > 0) {
return port;
}
}
if (port_from_host > 0) {
return port_from_host;
}

return https ? 443 : 80;
}

static void apply_scheme_and_port(__hashtable__ *server)
{
const char *host = env_or_empty("HTTP_HOST");
int https = sg_is_https_request();
const char *scheme = https ? "https" : "http";
char server_name[256];
int port_from_host = 0;
int port;
char port_buf[16];

if ('\0' != host[0]) {
set_string_key(server, "HTTP_HOST", host);
sg_parse_host_port(host, server_name, sizeof(server_name), &port_from_host);
if ('\0' != server_name[0]) {
set_string_key(server, "SERVER_NAME", server_name);
}
}

set_string_key(server, "REQUEST_SCHEME", scheme);
if (https) {
set_string_key(server, "HTTPS", "on");
}

port = sg_resolve_server_port(https, port_from_host);
snprintf(port_buf, sizeof(port_buf), "%d", port);
set_string_key(server, "SERVER_PORT", port_buf);
}

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 @@ -392,6 +507,7 @@ void __superglobals__refresh(void)
}

apply_cgi_headers_from_environ(sg_SERVER);
apply_scheme_and_port(sg_SERVER);

if (NULL == sg_COOKIE) {
sg_COOKIE = __hashtable__alloc();
Expand Down
11 changes: 11 additions & 0 deletions lib/JIT/SuperglobalInit.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ final class SuperglobalInit
/** @var array<string, \PHPLLVM\Value> */
public static array $globals = [];

/** $_SERVER keys repopulated by __superglobals__refresh (issue #201, #235). */
private const RUNTIME_SERVER_KEYS = [
'REQUEST_SCHEME',
'HTTPS',
'SERVER_PORT',
'SERVER_NAME',
];

public static function declareRefresh(Context $context): void
{
$signature = $context->context->functionType($context->context->voidType(), false);
Expand Down Expand Up @@ -148,6 +156,9 @@ public static function compileTimeReadString(
string $superglobalName,
string $key
): ?\PHPLLVM\Value {
if ('_SERVER' === $superglobalName && in_array($key, self::RUNTIME_SERVER_KEYS, true)) {
return null;
}
if (!self::compileTimeOffsetIsSet($context, $superglobalName, $key)) {
return null;
}
Expand Down
109 changes: 109 additions & 0 deletions lib/Web/Superglobals.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,115 @@ private static function populateServer(
self::setStringEntry($server, $key, $value);
}
}

self::applySchemeAndPort($server);
}

/**
* Derive REQUEST_SCHEME, HTTPS, SERVER_PORT, and SERVER_NAME (issue #235).
*/
public static function applySchemeAndPort(HashTable $server): void
{
$host = self::readStringEntry($server, 'HTTP_HOST');
if ('' === $host) {
$fromEnv = getenv('HTTP_HOST');
$host = false === $fromEnv ? '' : $fromEnv;
if ('' !== $host) {
self::setStringEntry($server, 'HTTP_HOST', $host);
}
}

$https = self::detectHttps($server);
$scheme = $https ? 'https' : 'http';
self::setStringEntry($server, 'REQUEST_SCHEME', $scheme);
if ($https) {
self::setStringEntry($server, 'HTTPS', 'on');
}

[$serverName, $portFromHost] = self::parseHostAndPort($host);
$port = self::resolveServerPort($https, $portFromHost);
self::setStringEntry($server, 'SERVER_PORT', (string) $port);

if ('' !== $serverName) {
self::setStringEntry($server, 'SERVER_NAME', $serverName);
} elseif ('' !== $host) {
self::setStringEntry($server, 'SERVER_NAME', $host);
}
}

/**
* @return array{0: string, 1: ?int} server name and optional port from Host header
*/
public static function parseHostAndPort(string $host): array
{
if ('' === $host) {
return ['', null];
}
if ('[' === $host[0]) {
$close = strpos($host, ']');
if (false !== $close) {
$name = substr($host, 1, $close - 1);
if (isset($host[$close + 1]) && ':' === $host[$close + 1]) {
$port = (int) substr($host, $close + 2);

return [$name, $port > 0 ? $port : null];
}

return [$name, null];
}
}
$colon = strrpos($host, ':');
if (false !== $colon && false === strpos($host, ':', $colon + 1)) {
$port = (int) substr($host, $colon + 1);
if ($port > 0) {
return [substr($host, 0, $colon), $port];
}
}

return [$host, null];
}

public static function detectHttps(HashTable $server): bool
{
$https = getenv('HTTPS');
if (false !== $https && '' !== $https && '0' !== $https && 'off' !== strtolower($https)) {
return true;
}

$proto = self::readStringEntry($server, 'HTTP_X_FORWARDED_PROTO');
if ('' === $proto) {
$fromEnv = getenv('HTTP_X_FORWARDED_PROTO');
$proto = false === $fromEnv ? '' : $fromEnv;
}

return 'https' === strtolower($proto);
}

private static function resolveServerPort(bool $https, ?int $portFromHost): int
{
$fromEnv = getenv('SERVER_PORT');
if (false !== $fromEnv && '' !== $fromEnv && ctype_digit($fromEnv)) {
return (int) $fromEnv;
}
if (null !== $portFromHost && $portFromHost > 0) {
return $portFromHost;
}

return $https ? 443 : 80;
}

private static function readStringEntry(HashTable $ht, string $key): string
{
$var = $ht->find($key);
if (null === $var) {
return '';
}
$resolved = $var->resolveIndirect();
if (Variable::TYPE_STRING !== $resolved->type) {
return '';
}

return $resolved->toString();
}

/**
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 testHttpsSchemeFromCgiEnvironment(): void
{
$source = <<<'PHP'
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
echo $_SERVER['REQUEST_SCHEME'], '://', $_SERVER['HTTP_HOST'];
PHP;

$outfile = tempnam(sys_get_temp_dir(), 'phpc_https_');
$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_FORWARDED_PROTO'] = 'https';
$runEnv['SCRIPT_NAME'] = '/index.php';
$runEnv['REQUEST_URI'] = '/index.php';
$output = $this->runBinary($outfile, $runEnv);
$this->assertStringContainsString('https://example.test', $output);

@unlink($outfile);
}

public function testHttpHostFromCgiEnvironment(): void
{
$source = <<<'PHP'
Expand Down
19 changes: 19 additions & 0 deletions test/real/cases/web_server_https.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
--TEST--
Web: REQUEST_SCHEME and HTTP_HOST for absolute URLs (issue #235)
--ENV--
REQUEST_METHOD=GET
SCRIPT_NAME=/index.php
REQUEST_URI=/index.php
HTTP_HOST=example.test
HTTP_X_FORWARDED_PROTO=https
--FILE--
<?php
echo $_SERVER['REQUEST_SCHEME'], '://', $_SERVER['HTTP_HOST'], "\n";
echo $_SERVER['HTTPS'], "\n";
echo $_SERVER['SERVER_PORT'], "\n";
echo $_SERVER['SERVER_NAME'], "\n";
--EXPECT--
https://example.test
on
443
example.test
Loading
Loading