Skip to content
54 changes: 53 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,12 @@ 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 +286,45 @@ 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