Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UriFactory::createFromSapi() #123

Merged
merged 2 commits into from
Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
233 changes: 1 addition & 232 deletions src/ServerRequestFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,8 @@
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;

use function array_change_key_case;
use function array_key_exists;
use function explode;
use function gettype;
use function implode;
use function is_array;
use function is_bool;
use function is_callable;
use function is_string;
use function ltrim;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_contains;
use function strlen;
use function strrpos;
use function strtolower;
use function substr;

use const CASE_LOWER;

/**
* Class for marshaling a request object from the current PHP environment.
Expand Down Expand Up @@ -88,7 +70,7 @@ public static function fromGlobals(
return $requestFilter(new ServerRequest(
$server,
$files,
self::marshalUriFromSapi($server, $headers),
UriFactory::createFromSapi($server, $headers),
marshalMethodFromSapi($server),
'php://input',
$headers,
Expand All @@ -114,217 +96,4 @@ public function createServerRequest(string $method, $uri, array $serverParams =
'php://temp'
);
}

/**
* Marshal a Uri instance based on the values present in the $_SERVER array and headers.
*
* @param array<string, string|list<string>> $headers
* @param array $server SAPI parameters
*/
private static function marshalUriFromSapi(array $server, array $headers): Uri
{
$uri = new Uri('');

// URI scheme
$https = false;
if (array_key_exists('HTTPS', $server)) {
$https = self::marshalHttpsValue($server['HTTPS']);
} elseif (array_key_exists('https', $server)) {
$https = self::marshalHttpsValue($server['https']);
}

$uri = $uri->withScheme($https ? 'https' : 'http');

// Set the host
[$host, $port] = self::marshalHostAndPort($server, $headers);
if (! empty($host)) {
$uri = $uri->withHost($host);
if (! empty($port)) {
$uri = $uri->withPort($port);
}
}

// URI path
$path = self::marshalRequestPath($server);

// Strip query string
$path = explode('?', $path, 2)[0];

// URI query
$query = '';
if (isset($server['QUERY_STRING'])) {
$query = ltrim((string) $server['QUERY_STRING'], '?');
}

// URI fragment
$fragment = '';
if (str_contains($path, '#')) {
[$path, $fragment] = explode('#', $path, 2);
}

return $uri
->withPath($path)
->withFragment($fragment)
->withQuery($query);
}

/**
* Marshal the host and port from the PHP environment.
*
* @param array<string, string|list<string>> $headers
* @return array{string, int|null} Array of two items, host and port,
* in that order (can be passed to a list() operation).
*/
private static function marshalHostAndPort(array $server, array $headers): array
{
static $defaults = ['', null];

$host = self::getHeaderFromArray('host', $headers, false);
if ($host !== false) {
// Ignore obviously malformed host headers:
// - Whitespace is invalid within a hostname and break the URI representation within HTTP.
// non-printable characters other than SPACE and TAB are already rejected by HeaderSecurity.
// - A comma indicates that multiple host headers have been sent which is not legal
// and might be used in an attack where a load balancer sees a different host header
// than Diactoros.
if (! preg_match('/[\\t ,]/', $host)) {
return self::marshalHostAndPortFromHeader($host);
}
}

if (! isset($server['SERVER_NAME'])) {
return $defaults;
}

$host = (string) $server['SERVER_NAME'];
$port = isset($server['SERVER_PORT']) ? (int) $server['SERVER_PORT'] : null;

if (
! isset($server['SERVER_ADDR'])
|| ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $host)
) {
return [$host, $port];
}

// Misinterpreted IPv6-Address
// Reported for Safari on Windows
return self::marshalIpv6HostAndPort($server, $port);
}

/**
* @return array{string, int|null} Array of two items, host and port,
* in that order (can be passed to a list() operation).
*/
private static function marshalIpv6HostAndPort(array $server, ?int $port): array
{
$host = '[' . (string) $server['SERVER_ADDR'] . ']';
$port = $port ?: 80;
$portSeparatorPos = strrpos($host, ':');

if (false === $portSeparatorPos) {
return [$host, $port];
}

if ($port . ']' === substr($host, $portSeparatorPos + 1)) {
// The last digit of the IPv6-Address has been taken as port
// Unset the port so the default port can be used
$port = null;
}
return [$host, $port];
}

/**
* Detect the path for the request
*
* Looks at a variety of criteria in order to attempt to autodetect the base
* request path, including:
*
* - IIS7 UrlRewrite environment
* - REQUEST_URI
* - ORIG_PATH_INFO
*/
private static function marshalRequestPath(array $server): string
{
// IIS7 with URL Rewrite: make sure we get the unencoded url
// (double slash problem).
$iisUrlRewritten = $server['IIS_WasUrlRewritten'] ?? null;
$unencodedUrl = $server['UNENCODED_URL'] ?? '';
if ('1' === $iisUrlRewritten && is_string($unencodedUrl) && '' !== $unencodedUrl) {
return $unencodedUrl;
}

$requestUri = $server['REQUEST_URI'] ?? null;

if (is_string($requestUri)) {
return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri);
}

$origPathInfo = $server['ORIG_PATH_INFO'] ?? '';
if (! is_string($origPathInfo) || '' === $origPathInfo) {
return '/';
}

return $origPathInfo;
}

private static function marshalHttpsValue(mixed $https): bool
{
if (is_bool($https)) {
return $https;
}

if (! is_string($https)) {
throw new Exception\InvalidArgumentException(sprintf(
'SAPI HTTPS value MUST be a string or boolean; received %s',
gettype($https)
));
}

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

/**
* @param string|list<string> $host
* @return array Array of two items, host and port, in that order (can be
* passed to a list() operation).
*/
private static function marshalHostAndPortFromHeader($host): array
{
if (is_array($host)) {
$host = implode(', ', $host);
}

$port = null;

// works for regname, IPv4 & IPv6
if (preg_match('|\:(\d+)$|', $host, $matches)) {
$host = substr($host, 0, -1 * (strlen($matches[1]) + 1));
$port = (int) $matches[1];
}

return [$host, $port];
}

/**
* Retrieve a header value from an array of headers using a case-insensitive lookup.
*
* @template T
* @param array<string, string|list<string>> $headers Key/value header pairs
* @param T $default Default value to return if header not found
* @return string|T
*/
private static function getHeaderFromArray(string $name, array $headers, $default = null)
{
$header = strtolower($name);
$headers = array_change_key_case($headers, CASE_LOWER);
if (! array_key_exists($header, $headers)) {
return $default;
}

if (is_string($headers[$header])) {
return $headers[$header];
}

return implode(', ', $headers[$header]);
}
}
Loading