Skip to content

Commit c0e1f4d

Browse files
committed
Move parsing incoming HTTP response message to Response
1 parent 5896f81 commit c0e1f4d

File tree

3 files changed

+146
-4
lines changed

3 files changed

+146
-4
lines changed

src/Io/ClientRequestStream.php

+10-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use React\Http\Message\Response;
99
use React\Socket\ConnectionInterface;
1010
use React\Stream\WritableStreamInterface;
11-
use RingCentral\Psr7 as gPsr;
1211

1312
/**
1413
* @event response
@@ -152,10 +151,17 @@ public function handleData($data)
152151
$this->buffer .= $data;
153152

154153
// buffer until double CRLF (or double LF for compatibility with legacy servers)
155-
if (false !== strpos($this->buffer, "\r\n\r\n") || false !== strpos($this->buffer, "\n\n")) {
154+
$eom = \strpos($this->buffer, "\r\n\r\n");
155+
$eomLegacy = \strpos($this->buffer, "\n\n");
156+
if ($eom !== false || $eomLegacy !== false) {
156157
try {
157-
$response = gPsr\parse_response($this->buffer);
158-
$bodyChunk = (string) $response->getBody();
158+
if ($eom !== false && ($eomLegacy === false || $eom < $eomLegacy)) {
159+
$response = Response::parseMessage(\substr($this->buffer, 0, $eom + 2));
160+
$bodyChunk = (string) \substr($this->buffer, $eom + 4);
161+
} else {
162+
$response = Response::parseMessage(\substr($this->buffer, 0, $eomLegacy + 1));
163+
$bodyChunk = (string) \substr($this->buffer, $eomLegacy + 2);
164+
}
159165
} catch (\InvalidArgumentException $exception) {
160166
$this->closeError($exception);
161167
return;

src/Message/Response.php

+42
Original file line numberDiff line numberDiff line change
@@ -369,4 +369,46 @@ private static function getReasonPhraseForStatusCode($code)
369369

370370
return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : '';
371371
}
372+
373+
/**
374+
* [Internal] Parse incoming HTTP protocol message
375+
*
376+
* @internal
377+
* @param string $message
378+
* @return self
379+
* @throws \InvalidArgumentException if given $message is not a valid HTTP response message
380+
*/
381+
public static function parseMessage($message)
382+
{
383+
$start = array();
384+
if (!\preg_match('#^HTTP/(?<version>\d\.\d) (?<status>\d{3})(?: (?<reason>[^\r\n]*+))?[\r]?+\n#m', $message, $start)) {
385+
throw new \InvalidArgumentException('Unable to parse invalid status-line');
386+
}
387+
388+
// only support HTTP/1.1 and HTTP/1.0 requests
389+
if ($start['version'] !== '1.1' && $start['version'] !== '1.0') {
390+
throw new \InvalidArgumentException('Received response with invalid protocol version');
391+
}
392+
393+
// check number of valid header fields matches number of lines + status line
394+
$matches = array();
395+
$n = \preg_match_all(self::REGEX_HEADERS, $message, $matches, \PREG_SET_ORDER);
396+
if (\substr_count($message, "\n") !== $n + 1) {
397+
throw new \InvalidArgumentException('Unable to parse invalid response header fields');
398+
}
399+
400+
// format all header fields into associative array
401+
$headers = array();
402+
foreach ($matches as $match) {
403+
$headers[$match[1]][] = $match[2];
404+
}
405+
406+
return new self(
407+
(int) $start['status'],
408+
$headers,
409+
'',
410+
$start['version'],
411+
isset($start['reason']) ? $start['reason'] : ''
412+
);
413+
}
372414
}

tests/Message/ResponseTest.php

+94
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,98 @@ public function testXmlMethodReturnsXmlResponse()
157157
$this->assertEquals('application/xml', $response->getHeaderLine('Content-Type'));
158158
$this->assertEquals('<?xml version="1.0" encoding="utf-8"?><body>Hello wörld!</body>', (string) $response->getBody());
159159
}
160+
161+
public function testParseMessageWithMinimalOkResponse()
162+
{
163+
$response = Response::parseMessage("HTTP/1.1 200 OK\r\n");
164+
165+
$this->assertEquals('1.1', $response->getProtocolVersion());
166+
$this->assertEquals(200, $response->getStatusCode());
167+
$this->assertEquals('OK', $response->getReasonPhrase());
168+
$this->assertEquals(array(), $response->getHeaders());
169+
}
170+
171+
public function testParseMessageWithSimpleOkResponse()
172+
{
173+
$response = Response::parseMessage("HTTP/1.1 200 OK\r\nServer: demo\r\n");
174+
175+
$this->assertEquals('1.1', $response->getProtocolVersion());
176+
$this->assertEquals(200, $response->getStatusCode());
177+
$this->assertEquals('OK', $response->getReasonPhrase());
178+
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
179+
}
180+
181+
public function testParseMessageWithSimpleOkResponseWithCustomReasonPhrase()
182+
{
183+
$response = Response::parseMessage("HTTP/1.1 200 Mostly Okay\r\nServer: demo\r\n");
184+
185+
$this->assertEquals('1.1', $response->getProtocolVersion());
186+
$this->assertEquals(200, $response->getStatusCode());
187+
$this->assertEquals('Mostly Okay', $response->getReasonPhrase());
188+
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
189+
}
190+
191+
public function testParseMessageWithSimpleOkResponseWithEmptyReasonPhraseAppliesDefault()
192+
{
193+
$response = Response::parseMessage("HTTP/1.1 200 \r\nServer: demo\r\n");
194+
195+
$this->assertEquals('1.1', $response->getProtocolVersion());
196+
$this->assertEquals(200, $response->getStatusCode());
197+
$this->assertEquals('OK', $response->getReasonPhrase());
198+
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
199+
}
200+
201+
public function testParseMessageWithSimpleOkResponseWithoutReasonPhraseAndWhitespaceSeparatorAppliesDefault()
202+
{
203+
$response = Response::parseMessage("HTTP/1.1 200\r\nServer: demo\r\n");
204+
205+
$this->assertEquals('1.1', $response->getProtocolVersion());
206+
$this->assertEquals(200, $response->getStatusCode());
207+
$this->assertEquals('OK', $response->getReasonPhrase());
208+
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
209+
}
210+
211+
public function testParseMessageWithHttp10SimpleOkResponse()
212+
{
213+
$response = Response::parseMessage("HTTP/1.0 200 OK\r\nServer: demo\r\n");
214+
215+
$this->assertEquals('1.0', $response->getProtocolVersion());
216+
$this->assertEquals(200, $response->getStatusCode());
217+
$this->assertEquals('OK', $response->getReasonPhrase());
218+
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
219+
}
220+
221+
public function testParseMessageWithHttp10SimpleOkResponseWithLegacyNewlines()
222+
{
223+
$response = Response::parseMessage("HTTP/1.0 200 OK\nServer: demo\r\n");
224+
225+
$this->assertEquals('1.0', $response->getProtocolVersion());
226+
$this->assertEquals(200, $response->getStatusCode());
227+
$this->assertEquals('OK', $response->getReasonPhrase());
228+
$this->assertEquals(array('Server' => array('demo')), $response->getHeaders());
229+
}
230+
231+
public function testParseMessageWithInvalidHttpProtocolVersion12Throws()
232+
{
233+
$this->setExpectedException('InvalidArgumentException');
234+
Response::parseMessage("HTTP/1.2 200 OK\r\n");
235+
}
236+
237+
public function testParseMessageWithInvalidHttpProtocolVersion2Throws()
238+
{
239+
$this->setExpectedException('InvalidArgumentException');
240+
Response::parseMessage("HTTP/2 200 OK\r\n");
241+
}
242+
243+
public function testParseMessageWithInvalidStatusCodeUnderflowThrows()
244+
{
245+
$this->setExpectedException('InvalidArgumentException');
246+
Response::parseMessage("HTTP/1.1 99 OK\r\n");
247+
}
248+
249+
public function testParseMessageWithInvalidResponseHeaderFieldThrows()
250+
{
251+
$this->setExpectedException('InvalidArgumentException');
252+
Response::parseMessage("HTTP/1.1 200 OK\r\nServer\r\n");
253+
}
160254
}

0 commit comments

Comments
 (0)