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

[5.3] - Fix JHTTP socket transport http version #43130

Open
wants to merge 13 commits into
base: 5.3-dev
Choose a base branch
from
58 changes: 57 additions & 1 deletion libraries/src/Http/Transport/SocketTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public function request($method, UriInterface $uri, $data = null, array $headers

// Build the request payload.
$request = [];
$request[] = strtoupper($method) . ' ' . ((empty($path)) ? '/' : $path) . ' HTTP/1.0';
$request[] = strtoupper($method) . ' ' . ((empty($path)) ? '/' : $path) . ' HTTP/1.1';
$request[] = 'Host: ' . $uri->getHost();

// If an explicit user agent is given use it.
Expand All @@ -101,6 +101,11 @@ public function request($method, UriInterface $uri, $data = null, array $headers
}
}

// HTTP/1.1 streams using the socket wrapper require a Connection: close header
if (!isset($headers['Connection'])) {
$request[] = 'Connection: close';
}

// Set any custom transport options
foreach ($this->getOption('transport.socket', []) as $value) {
$request[] = $value;
Expand Down Expand Up @@ -174,6 +179,14 @@ protected function getResponse($content)
$statusCode = (int) $code;
$verifiedHeaders = $this->processHeaders($headers);

// If we have a HTTP 1.1 Response with chunked encoding then we have to decode the message
if (
\array_key_exists('Transfer-Encoding', $verifiedHeaders)
&& $verifiedHeaders['Transfer-Encoding'][0] === 'chunked'
) {
$body = static::httpChunkedDecode($body);
}

$streamInterface = new StreamResponse('php://memory', 'rw');
$streamInterface->write($body);

Expand Down Expand Up @@ -275,4 +288,47 @@ public static function isSupported()
{
return \function_exists('fsockopen') && \is_callable('fsockopen') && !Factory::getApplication()->get('proxy_enable');
}

/**
* De-chunks a http 'transfer-encoding: chunked' message for when decoding a HTTP 1.1 server message.
*
* @param string $chunk The encoded message
*
* @return string The decoded message. If $chunk wasn't encoded properly it will be returned unmodified.
*/
public static function httpChunkedDecode(string $chunk): string
{
$pos = 0;
$len = \strlen($chunk);
$resp = '';

while (
($pos < $len)
&& ($chunkLenHex = substr($chunk, $pos, ($newlineAt = strpos($chunk, "\n", $pos + 1)) - $pos))
) {
if (!static::isHex(rtrim($chunkLenHex))) {
trigger_error('Value is not properly chunk encoded', E_USER_WARNING);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that should be an exception, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure. When it's hard to find examples of when this might fail - hard to know if it can ever not be a hex in genuine page output.

return $chunk;
}

$pos = $newlineAt++;
$chunkLen = hexdec(rtrim($chunkLenHex, "\r\n"));
$resp .= substr($chunk, $pos + 1, $chunkLen);
$pos = strpos($chunk, "\n", $pos + $chunkLen) + 1;
}

return $resp;
}

/**
* Determine if a string can represent a number in hexadecimal
*
* @param string $hex
*
* @return boolean
*/
private static function isHex(string $hex): bool
{
return empty($hex) || (@preg_match("/^[a-f0-9]{2,}$/i", $hex) && !(\strlen($hex) & 1));
}
}
9 changes: 9 additions & 0 deletions libraries/src/Http/Transport/StreamTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ public function request($method, UriInterface $uri, $data = null, array $headers
$options[$key] = $value;
}

if (!\array_key_exists('protocol_version', $options)) {
$options['protocol_version'] = '1.1';
}

// HTTP/1.1 streams using the PHP stream wrapper require a Connection: close header
if ($options['protocol_version'] == '1.1' && !isset($headers['Connection'])) {
$headers['Connection'] = 'close';
}

// Add the proxy configuration, if any.
$app = Factory::getApplication();

Expand Down