Skip to content

Commit

Permalink
Move parsing incoming HTTP response message to Response
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Mar 22, 2024
1 parent 5896f81 commit c0e1f4d
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 4 deletions.
14 changes: 10 additions & 4 deletions src/Io/ClientRequestStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use React\Http\Message\Response;
use React\Socket\ConnectionInterface;
use React\Stream\WritableStreamInterface;
use RingCentral\Psr7 as gPsr;

/**
* @event response
Expand Down Expand Up @@ -152,10 +151,17 @@ public function handleData($data)
$this->buffer .= $data;

// buffer until double CRLF (or double LF for compatibility with legacy servers)
if (false !== strpos($this->buffer, "\r\n\r\n") || false !== strpos($this->buffer, "\n\n")) {
$eom = \strpos($this->buffer, "\r\n\r\n");
$eomLegacy = \strpos($this->buffer, "\n\n");
if ($eom !== false || $eomLegacy !== false) {
try {
$response = gPsr\parse_response($this->buffer);
$bodyChunk = (string) $response->getBody();
if ($eom !== false && ($eomLegacy === false || $eom < $eomLegacy)) {
$response = Response::parseMessage(\substr($this->buffer, 0, $eom + 2));
$bodyChunk = (string) \substr($this->buffer, $eom + 4);
} else {
$response = Response::parseMessage(\substr($this->buffer, 0, $eomLegacy + 1));
$bodyChunk = (string) \substr($this->buffer, $eomLegacy + 2);
}
} catch (\InvalidArgumentException $exception) {
$this->closeError($exception);
return;
Expand Down
42 changes: 42 additions & 0 deletions src/Message/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,46 @@ private static function getReasonPhraseForStatusCode($code)

return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : '';
}

/**
* [Internal] Parse incoming HTTP protocol message
*
* @internal
* @param string $message
* @return self
* @throws \InvalidArgumentException if given $message is not a valid HTTP response message
*/
public static function parseMessage($message)
{
$start = array();
if (!\preg_match('#^HTTP/(?<version>\d\.\d) (?<status>\d{3})(?: (?<reason>[^\r\n]*+))?[\r]?+\n#m', $message, $start)) {
throw new \InvalidArgumentException('Unable to parse invalid status-line');
}

// only support HTTP/1.1 and HTTP/1.0 requests
if ($start['version'] !== '1.1' && $start['version'] !== '1.0') {
throw new \InvalidArgumentException('Received response with invalid protocol version');
}

// check number of valid header fields matches number of lines + status line
$matches = array();
$n = \preg_match_all(self::REGEX_HEADERS, $message, $matches, \PREG_SET_ORDER);
if (\substr_count($message, "\n") !== $n + 1) {
throw new \InvalidArgumentException('Unable to parse invalid response header fields');
}

// format all header fields into associative array
$headers = array();
foreach ($matches as $match) {
$headers[$match[1]][] = $match[2];
}

return new self(
(int) $start['status'],
$headers,
'',
$start['version'],
isset($start['reason']) ? $start['reason'] : ''
);
}
}
94 changes: 94 additions & 0 deletions tests/Message/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,98 @@ public function testXmlMethodReturnsXmlResponse()
$this->assertEquals('application/xml', $response->getHeaderLine('Content-Type'));
$this->assertEquals('<?xml version="1.0" encoding="utf-8"?><body>Hello wörld!</body>', (string) $response->getBody());
}

public function testParseMessageWithMinimalOkResponse()
{
$response = Response::parseMessage("HTTP/1.1 200 OK\r\n");

$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('OK', $response->getReasonPhrase());
$this->assertEquals(array(), $response->getHeaders());
}

public function testParseMessageWithSimpleOkResponse()
{
$response = Response::parseMessage("HTTP/1.1 200 OK\r\nServer: demo\r\n");

$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('OK', $response->getReasonPhrase());
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
}

public function testParseMessageWithSimpleOkResponseWithCustomReasonPhrase()
{
$response = Response::parseMessage("HTTP/1.1 200 Mostly Okay\r\nServer: demo\r\n");

$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('Mostly Okay', $response->getReasonPhrase());
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
}

public function testParseMessageWithSimpleOkResponseWithEmptyReasonPhraseAppliesDefault()
{
$response = Response::parseMessage("HTTP/1.1 200 \r\nServer: demo\r\n");

$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('OK', $response->getReasonPhrase());
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
}

public function testParseMessageWithSimpleOkResponseWithoutReasonPhraseAndWhitespaceSeparatorAppliesDefault()
{
$response = Response::parseMessage("HTTP/1.1 200\r\nServer: demo\r\n");

$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('OK', $response->getReasonPhrase());
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
}

public function testParseMessageWithHttp10SimpleOkResponse()
{
$response = Response::parseMessage("HTTP/1.0 200 OK\r\nServer: demo\r\n");

$this->assertEquals('1.0', $response->getProtocolVersion());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('OK', $response->getReasonPhrase());
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
}

public function testParseMessageWithHttp10SimpleOkResponseWithLegacyNewlines()
{
$response = Response::parseMessage("HTTP/1.0 200 OK\nServer: demo\r\n");

$this->assertEquals('1.0', $response->getProtocolVersion());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('OK', $response->getReasonPhrase());
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
}

public function testParseMessageWithInvalidHttpProtocolVersion12Throws()
{
$this->setExpectedException('InvalidArgumentException');
Response::parseMessage("HTTP/1.2 200 OK\r\n");
}

public function testParseMessageWithInvalidHttpProtocolVersion2Throws()
{
$this->setExpectedException('InvalidArgumentException');
Response::parseMessage("HTTP/2 200 OK\r\n");
}

public function testParseMessageWithInvalidStatusCodeUnderflowThrows()
{
$this->setExpectedException('InvalidArgumentException');
Response::parseMessage("HTTP/1.1 99 OK\r\n");
}

public function testParseMessageWithInvalidResponseHeaderFieldThrows()
{
$this->setExpectedException('InvalidArgumentException');
Response::parseMessage("HTTP/1.1 200 OK\r\nServer\r\n");
}
}

0 comments on commit c0e1f4d

Please sign in to comment.