diff --git a/src/Io/AbstractMessage.php b/src/Io/AbstractMessage.php index 8523d6cd..ab023304 100644 --- a/src/Io/AbstractMessage.php +++ b/src/Io/AbstractMessage.php @@ -13,6 +13,14 @@ */ abstract class AbstractMessage implements MessageInterface { + /** + * [Internal] Regex used to match all request header fields into an array, thanks to @kelunik for checking the HTTP specs and coming up with this regex + * + * @internal + * @var string + */ + const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x01-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m'; + /** @var array */ private $headers = array(); diff --git a/src/Io/ClientRequestStream.php b/src/Io/ClientRequestStream.php index 0220f008..25c96ea8 100644 --- a/src/Io/ClientRequestStream.php +++ b/src/Io/ClientRequestStream.php @@ -8,7 +8,6 @@ use React\Http\Message\Response; use React\Socket\ConnectionInterface; use React\Stream\WritableStreamInterface; -use RingCentral\Psr7 as gPsr; /** * @event response @@ -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; diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index b8336f5b..8975ce57 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -128,39 +128,6 @@ public function handle(ConnectionInterface $conn) */ public function parseRequest($headers, ConnectionInterface $connection) { - // additional, stricter safe-guard for request line - // because request parser doesn't properly cope with invalid ones - $start = array(); - if (!\preg_match('#^(?[^ ]+) (?[^ ]+) HTTP/(?\d\.\d)#m', $headers, $start)) { - throw new \InvalidArgumentException('Unable to parse invalid request-line'); - } - - // only support HTTP/1.1 and HTTP/1.0 requests - if ($start['version'] !== '1.1' && $start['version'] !== '1.0') { - throw new \InvalidArgumentException('Received request with invalid protocol version', Response::STATUS_VERSION_NOT_SUPPORTED); - } - - // match all request header fields into array, thanks to @kelunik for checking the HTTP specs and coming up with this regex - $matches = array(); - $n = \preg_match_all('/^([^()<>@,;:\\\"\/\[\]?={}\x01-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m', $headers, $matches, \PREG_SET_ORDER); - - // check number of valid header fields matches number of lines + request line - if (\substr_count($headers, "\n") !== $n + 1) { - throw new \InvalidArgumentException('Unable to parse invalid request header fields'); - } - - // format all header fields into associative array - $host = null; - $fields = array(); - foreach ($matches as $match) { - $fields[$match[1]][] = $match[2]; - - // match `Host` request header - if ($host === null && \strtolower($match[1]) === 'host') { - $host = $match[2]; - } - } - // reuse same connection params for all server params for this connection $cid = \PHP_VERSION_ID < 70200 ? \spl_object_hash($connection) : \spl_object_id($connection); if (isset($this->connectionParams[$cid])) { @@ -207,101 +174,6 @@ public function parseRequest($headers, ConnectionInterface $connection) $serverParams['REQUEST_TIME'] = (int) ($now = $this->clock->now()); $serverParams['REQUEST_TIME_FLOAT'] = $now; - // scheme is `http` unless TLS is used - $scheme = isset($serverParams['HTTPS']) ? 'https://' : 'http://'; - - // default host if unset comes from local socket address or defaults to localhost - $hasHost = $host !== null; - if ($host === null) { - $host = isset($serverParams['SERVER_ADDR'], $serverParams['SERVER_PORT']) ? $serverParams['SERVER_ADDR'] . ':' . $serverParams['SERVER_PORT'] : '127.0.0.1'; - } - - if ($start['method'] === 'OPTIONS' && $start['target'] === '*') { - // support asterisk-form for `OPTIONS *` request line only - $uri = $scheme . $host; - } elseif ($start['method'] === 'CONNECT') { - $parts = \parse_url('tcp://' . $start['target']); - - // check this is a valid authority-form request-target (host:port) - if (!isset($parts['scheme'], $parts['host'], $parts['port']) || \count($parts) !== 3) { - throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target'); - } - $uri = $scheme . $start['target']; - } else { - // support absolute-form or origin-form for proxy requests - if ($start['target'][0] === '/') { - $uri = $scheme . $host . $start['target']; - } else { - // ensure absolute-form request-target contains a valid URI - $parts = \parse_url($start['target']); - - // make sure value contains valid host component (IP or hostname), but no fragment - if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) { - throw new \InvalidArgumentException('Invalid absolute-form request-target'); - } - - $uri = $start['target']; - } - } - - $request = new ServerRequest( - $start['method'], - $uri, - $fields, - '', - $start['version'], - $serverParams - ); - - // only assign request target if it is not in origin-form (happy path for most normal requests) - if ($start['target'][0] !== '/') { - $request = $request->withRequestTarget($start['target']); - } - - if ($hasHost) { - // Optional Host request header value MUST be valid (host and optional port) - $parts = \parse_url('http://' . $request->getHeaderLine('Host')); - - // make sure value contains valid host component (IP or hostname) - if (!$parts || !isset($parts['scheme'], $parts['host'])) { - $parts = false; - } - - // make sure value does not contain any other URI component - if (\is_array($parts)) { - unset($parts['scheme'], $parts['host'], $parts['port']); - } - if ($parts === false || $parts) { - throw new \InvalidArgumentException('Invalid Host header value'); - } - } elseif (!$hasHost && $start['version'] === '1.1' && $start['method'] !== 'CONNECT') { - // require Host request header for HTTP/1.1 (except for CONNECT method) - throw new \InvalidArgumentException('Missing required Host request header'); - } elseif (!$hasHost) { - // remove default Host request header for HTTP/1.0 when not explicitly given - $request = $request->withoutHeader('Host'); - } - - // ensure message boundaries are valid according to Content-Length and Transfer-Encoding request headers - if ($request->hasHeader('Transfer-Encoding')) { - if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') { - throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', Response::STATUS_NOT_IMPLEMENTED); - } - - // Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time - // as per https://tools.ietf.org/html/rfc7230#section-3.3.3 - if ($request->hasHeader('Content-Length')) { - throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', Response::STATUS_BAD_REQUEST); - } - } elseif ($request->hasHeader('Content-Length')) { - $string = $request->getHeaderLine('Content-Length'); - - if ((string)(int)$string !== $string) { - // Content-Length value is not an integer or not a single integer - throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST); - } - } - - return $request; + return ServerRequest::parseMessage($headers, $serverParams); } } diff --git a/src/Message/Response.php b/src/Message/Response.php index 95c82ec8..fa6366ed 100644 --- a/src/Message/Response.php +++ b/src/Message/Response.php @@ -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/(?\d\.\d) (?\d{3})(?: (?[^\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'] : '' + ); + } } diff --git a/src/Message/ServerRequest.php b/src/Message/ServerRequest.php index b5c41413..32a0f62f 100644 --- a/src/Message/ServerRequest.php +++ b/src/Message/ServerRequest.php @@ -189,4 +189,143 @@ private function parseCookie($cookie) return $result; } + + /** + * [Internal] Parse incoming HTTP protocol message + * + * @internal + * @param string $message + * @param array $serverParams + * @return self + * @throws \InvalidArgumentException if given $message is not a valid HTTP request message + */ + public static function parseMessage($message, array $serverParams) + { + // parse request line like "GET /path HTTP/1.1" + $start = array(); + if (!\preg_match('#^(?[^ ]+) (?[^ ]+) HTTP/(?\d\.\d)#m', $message, $start)) { + throw new \InvalidArgumentException('Unable to parse invalid request-line'); + } + + // only support HTTP/1.1 and HTTP/1.0 requests + if ($start['version'] !== '1.1' && $start['version'] !== '1.0') { + throw new \InvalidArgumentException('Received request with invalid protocol version', Response::STATUS_VERSION_NOT_SUPPORTED); + } + + // check number of valid header fields matches number of lines + request 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 request header fields'); + } + + // format all header fields into associative array + $host = null; + $headers = array(); + foreach ($matches as $match) { + $headers[$match[1]][] = $match[2]; + + // match `Host` request header + if ($host === null && \strtolower($match[1]) === 'host') { + $host = $match[2]; + } + } + + // scheme is `http` unless TLS is used + $scheme = isset($serverParams['HTTPS']) ? 'https://' : 'http://'; + + // default host if unset comes from local socket address or defaults to localhost + $hasHost = $host !== null; + if ($host === null) { + $host = isset($serverParams['SERVER_ADDR'], $serverParams['SERVER_PORT']) ? $serverParams['SERVER_ADDR'] . ':' . $serverParams['SERVER_PORT'] : '127.0.0.1'; + } + + if ($start['method'] === 'OPTIONS' && $start['target'] === '*') { + // support asterisk-form for `OPTIONS *` request line only + $uri = $scheme . $host; + } elseif ($start['method'] === 'CONNECT') { + $parts = \parse_url('tcp://' . $start['target']); + + // check this is a valid authority-form request-target (host:port) + if (!isset($parts['scheme'], $parts['host'], $parts['port']) || \count($parts) !== 3) { + throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target'); + } + $uri = $scheme . $start['target']; + } else { + // support absolute-form or origin-form for proxy requests + if ($start['target'][0] === '/') { + $uri = $scheme . $host . $start['target']; + } else { + // ensure absolute-form request-target contains a valid URI + $parts = \parse_url($start['target']); + + // make sure value contains valid host component (IP or hostname), but no fragment + if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) { + throw new \InvalidArgumentException('Invalid absolute-form request-target'); + } + + $uri = $start['target']; + } + } + + $request = new self( + $start['method'], + $uri, + $headers, + '', + $start['version'], + $serverParams + ); + + // only assign request target if it is not in origin-form (happy path for most normal requests) + if ($start['target'][0] !== '/') { + $request = $request->withRequestTarget($start['target']); + } + + if ($hasHost) { + // Optional Host request header value MUST be valid (host and optional port) + $parts = \parse_url('http://' . $request->getHeaderLine('Host')); + + // make sure value contains valid host component (IP or hostname) + if (!$parts || !isset($parts['scheme'], $parts['host'])) { + $parts = false; + } + + // make sure value does not contain any other URI component + if (\is_array($parts)) { + unset($parts['scheme'], $parts['host'], $parts['port']); + } + if ($parts === false || $parts) { + throw new \InvalidArgumentException('Invalid Host header value'); + } + } elseif (!$hasHost && $start['version'] === '1.1' && $start['method'] !== 'CONNECT') { + // require Host request header for HTTP/1.1 (except for CONNECT method) + throw new \InvalidArgumentException('Missing required Host request header'); + } elseif (!$hasHost) { + // remove default Host request header for HTTP/1.0 when not explicitly given + $request = $request->withoutHeader('Host'); + } + + // ensure message boundaries are valid according to Content-Length and Transfer-Encoding request headers + if ($request->hasHeader('Transfer-Encoding')) { + if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') { + throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', Response::STATUS_NOT_IMPLEMENTED); + } + + // Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time + // as per https://tools.ietf.org/html/rfc7230#section-3.3.3 + if ($request->hasHeader('Content-Length')) { + throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', Response::STATUS_BAD_REQUEST); + } + } elseif ($request->hasHeader('Content-Length')) { + $string = $request->getHeaderLine('Content-Length'); + + if ((string)(int)$string !== $string) { + // Content-Length value is not an integer or not a single integer + throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST); + } + } + + return $request; + } } diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php index 88b56945..a9a244c2 100644 --- a/tests/Message/ResponseTest.php +++ b/tests/Message/ResponseTest.php @@ -157,4 +157,98 @@ public function testXmlMethodReturnsXmlResponse() $this->assertEquals('application/xml', $response->getHeaderLine('Content-Type')); $this->assertEquals('Hello wörld!', (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"); + } } diff --git a/tests/Message/ServerRequestTest.php b/tests/Message/ServerRequestTest.php index a5919f64..f82d60f8 100644 --- a/tests/Message/ServerRequestTest.php +++ b/tests/Message/ServerRequestTest.php @@ -362,4 +362,126 @@ public function testConstructWithResourceRequestBodyThrows() tmpfile() ); } + + public function testParseMessageWithSimpleGetRequest() + { + $request = ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: example.com\r\n", array()); + + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('http://example.com/', (string) $request->getUri()); + $this->assertEquals('1.1', $request->getProtocolVersion()); + } + + public function testParseMessageWithHttp10RequestWithoutHost() + { + $request = ServerRequest::parseMessage("GET / HTTP/1.0\r\n", array()); + + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('http://127.0.0.1/', (string) $request->getUri()); + $this->assertEquals('1.0', $request->getProtocolVersion()); + } + + public function testParseMessageWithOptionsMethodWithAsteriskFormRequestTarget() + { + $request = ServerRequest::parseMessage("OPTIONS * HTTP/1.1\r\nHost: example.com\r\n", array()); + + $this->assertEquals('OPTIONS', $request->getMethod()); + $this->assertEquals('*', $request->getRequestTarget()); + $this->assertEquals('1.1', $request->getProtocolVersion()); + $this->assertEquals('http://example.com', (string) $request->getUri()); + } + + public function testParseMessageWithConnectMethodWithAuthorityFormRequestTarget() + { + $request = ServerRequest::parseMessage("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n", array()); + + $this->assertEquals('CONNECT', $request->getMethod()); + $this->assertEquals('example.com:80', $request->getRequestTarget()); + $this->assertEquals('1.1', $request->getProtocolVersion()); + $this->assertEquals('http://example.com', (string) $request->getUri()); + } + + public function testParseMessageWithInvalidHttp11RequestWithoutHostThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / HTTP/1.1\r\n", array()); + } + + public function testParseMessageWithInvalidHttpProtocolVersionThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / HTTP/1.2\r\n", array()); + } + + public function testParseMessageWithInvalidProtocolThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / CUSTOM/1.1\r\n", array()); + } + + public function testParseMessageWithInvalidHostHeaderWithoutValueThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost\r\n", array()); + } + + public function testParseMessageWithInvalidHostHeaderSyntaxThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: ///\r\n", array()); + } + + public function testParseMessageWithInvalidHostHeaderWithSchemeThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: http://localhost\r\n", array()); + } + + public function testParseMessageWithInvalidHostHeaderWithQueryThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost?foo\r\n", array()); + } + + public function testParseMessageWithInvalidHostHeaderWithFragmentThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost#foo\r\n", array()); + } + + public function testParseMessageWithInvalidContentLengthHeaderThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length:\r\n", array()); + } + + public function testParseMessageWithInvalidTransferEncodingHeaderThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding:\r\n", array()); + } + + public function testParseMessageWithInvalidBothContentLengthHeaderAndTransferEncodingHeaderThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n", array()); + } + + public function testParseMessageWithInvalidEmptyHostHeaderWithAbsoluteFormRequestTargetThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET http://example.com/ HTTP/1.1\r\nHost: \r\n", array()); + } + + public function testParseMessageWithInvalidConnectMethodNotUsingAuthorityFormThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("CONNECT / HTTP/1.1\r\nHost: localhost\r\n", array()); + } + + public function testParseMessageWithInvalidRequestTargetAsteriskFormThrows() + { + $this->setExpectedException('InvalidArgumentException'); + ServerRequest::parseMessage("GET * HTTP/1.1\r\nHost: localhost\r\n", array()); + } }