diff --git a/src/Io/EmptyBodyStream.php b/src/Io/EmptyBodyStream.php new file mode 100644 index 00000000..5056219c --- /dev/null +++ b/src/Io/EmptyBodyStream.php @@ -0,0 +1,142 @@ +closed; + } + + public function pause() + { + // NOOP + } + + public function resume() + { + // NOOP + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function getSize() + { + return 0; + } + + /** @ignore */ + public function __toString() + { + return ''; + } + + /** @ignore */ + public function detach() + { + return null; + } + + /** @ignore */ + public function tell() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function eof() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isSeekable() + { + return false; + } + + /** @ignore */ + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function rewind() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isWritable() + { + return false; + } + + /** @ignore */ + public function write($string) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function read($length) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function getContents() + { + return ''; + } + + /** @ignore */ + public function getMetadata($key = null) + { + return ($key === null) ? array() : null; + } +} diff --git a/src/Io/RequestHeaderParser.php b/src/Io/RequestHeaderParser.php index 88a554d7..adcc184a 100644 --- a/src/Io/RequestHeaderParser.php +++ b/src/Io/RequestHeaderParser.php @@ -69,9 +69,42 @@ public function handle(ConnectionInterface $conn) return; } + $contentLength = 0; + if ($request->hasHeader('Transfer-Encoding')) { + $contentLength = null; + } elseif ($request->hasHeader('Content-Length')) { + $contentLength = (int)$request->getHeaderLine('Content-Length'); + } + + if ($contentLength === 0) { + // happy path: request body is known to be empty + $stream = new EmptyBodyStream(); + $request = $request->withBody($stream); + } else { + // otherwise body is present => delimit using Content-Length or ChunkedDecoder + $stream = new CloseProtectionStream($conn); + if ($contentLength !== null) { + $stream = new LengthLimitedStream($stream, $contentLength); + } else { + $stream = new ChunkedDecoder($stream); + } + + $request = $request->withBody(new HttpBodyStream($stream, $contentLength)); + } + $bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : ''; $buffer = ''; - $that->emit('headers', array($request, $bodyBuffer, $conn)); + $that->emit('headers', array($request, $conn)); + + if ($bodyBuffer !== '') { + $conn->emit('data', array($bodyBuffer)); + } + + // happy path: request body is known to be empty => immediately end stream + if ($contentLength === 0) { + $stream->emit('end'); + $stream->close(); + } }); $conn->on('close', function () use (&$buffer, &$fn) { @@ -135,7 +168,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) // apply SERVER_ADDR and SERVER_PORT if server address is known // address should always be known, even for Unix domain sockets (UDS) - // but skip UDS as it doesn't have a concept of host/port.s + // but skip UDS as it doesn't have a concept of host/port. if ($localSocketUri !== null) { $localAddress = \parse_url($localSocketUri); if (isset($localAddress['host'], $localAddress['port'])) { @@ -199,6 +232,26 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri) } } + // 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', 501); + } + + // 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', 400); + } + } 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', 400); + } + } + // set URI components from socket address if not already filled via Host header if ($request->getUri()->getHost() === '') { $parts = \parse_url($localSocketUri); diff --git a/src/StreamingServer.php b/src/StreamingServer.php index fa3f9777..3826dbc3 100644 --- a/src/StreamingServer.php +++ b/src/StreamingServer.php @@ -112,12 +112,8 @@ public function __construct($requestHandler) $this->parser = new RequestHeaderParser(); $that = $this; - $this->parser->on('headers', function (ServerRequestInterface $request, $bodyBuffer, ConnectionInterface $conn) use ($that) { + $this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) { $that->handleRequest($conn, $request); - - if ($bodyBuffer !== '') { - $conn->emit('data', array($bodyBuffer)); - } }); $this->parser->on('error', function(\Exception $e, ConnectionInterface $conn) use ($that) { @@ -182,38 +178,6 @@ public function listen(ServerInterface $socket) /** @internal */ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $request) { - $contentLength = 0; - $stream = new CloseProtectionStream($conn); - if ($request->hasHeader('Transfer-Encoding')) { - if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') { - $this->emit('error', array(new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding'))); - return $this->writeError($conn, 501, $request); - } - - // 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')) { - $this->emit('error', array(new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed'))); - return $this->writeError($conn, 400, $request); - } - - $stream = new ChunkedDecoder($stream); - $contentLength = null; - } elseif ($request->hasHeader('Content-Length')) { - $string = $request->getHeaderLine('Content-Length'); - - $contentLength = (int)$string; - if ((string)$contentLength !== $string) { - // Content-Length value is not an integer or not a single integer - $this->emit('error', array(new \InvalidArgumentException('The value of `Content-Length` is not valid'))); - return $this->writeError($conn, 400, $request); - } - - $stream = new LengthLimitedStream($stream, $contentLength); - } - - $request = $request->withBody(new HttpBodyStream($stream, $contentLength)); - if ($request->getProtocolVersion() !== '1.0' && '100-continue' === \strtolower($request->getHeaderLine('Expect'))) { $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); } @@ -237,12 +201,6 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface }); } - // happy path: request body is known to be empty => immediately end stream - if ($contentLength === 0) { - $stream->emit('end'); - $stream->close(); - } - // happy path: response returned, handle and return immediately if ($response instanceof ResponseInterface) { return $this->handleResponse($conn, $request, $response); diff --git a/tests/Io/EmptyBodyStreamTest.php b/tests/Io/EmptyBodyStreamTest.php new file mode 100644 index 00000000..6f309fa9 --- /dev/null +++ b/tests/Io/EmptyBodyStreamTest.php @@ -0,0 +1,152 @@ +bodyStream = new EmptyBodyStream(); + } + + /** + * @doesNotPerformAssertions + */ + public function testPauseIsNoop() + { + $this->bodyStream->pause(); + } + + /** + * @doesNotPerformAssertions + */ + public function testResumeIsNoop() + { + $this->bodyStream->resume(); + } + + public function testPipeStreamReturnsDestinationStream() + { + $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $ret = $this->bodyStream->pipe($dest); + + $this->assertSame($dest, $ret); + } + + public function testToStringReturnsEmptyString() + { + $this->assertEquals('', $this->bodyStream->__toString()); + } + + public function testDetachReturnsNull() + { + $this->assertNull($this->bodyStream->detach()); + } + + public function testGetSizeReturnsZero() + { + $this->assertSame(0, $this->bodyStream->getSize()); + } + + public function testCloseTwiceEmitsCloseEventAndClearsListeners() + { + $this->bodyStream->on('close', $this->expectCallableOnce()); + + $this->bodyStream->close(); + $this->bodyStream->close(); + + $this->assertEquals(array(), $this->bodyStream->listeners('close')); + } + + /** + * @expectedException BadMethodCallException + */ + public function testTell() + { + $this->bodyStream->tell(); + } + + /** + * @expectedException BadMethodCallException + */ + public function testEof() + { + $this->bodyStream->eof(); + } + + public function testIsSeekable() + { + $this->assertFalse($this->bodyStream->isSeekable()); + } + + /** + * @expectedException BadMethodCallException + */ + public function testWrite() + { + $this->bodyStream->write(''); + } + + /** + * @expectedException BadMethodCallException + */ + public function testRead() + { + $this->bodyStream->read(1); + } + + public function testGetContentsReturnsEmpy() + { + $this->assertEquals('', $this->bodyStream->getContents()); + } + + public function testGetMetaDataWithoutKeyReturnsEmptyArray() + { + $this->assertSame(array(), $this->bodyStream->getMetadata()); + } + + public function testGetMetaDataWithKeyReturnsNull() + { + $this->assertNull($this->bodyStream->getMetadata('anything')); + } + + public function testIsReadableReturnsTrueWhenNotClosed() + { + $this->assertTrue($this->bodyStream->isReadable()); + } + + public function testIsReadableReturnsFalseWhenAlreadyClosed() + { + $this->bodyStream->close(); + + $this->assertFalse($this->bodyStream->isReadable()); + } + + /** + * @expectedException BadMethodCallException + */ + public function testSeek() + { + $this->bodyStream->seek(''); + } + + /** + * @expectedException BadMethodCallException + */ + public function testRewind() + { + $this->bodyStream->rewind(); + } + + public function testIsWriteable() + { + $this->assertFalse($this->bodyStream->isWritable()); + } +} diff --git a/tests/Io/RequestHeaderParserTest.php b/tests/Io/RequestHeaderParserTest.php index dbf44e69..8bbb1603 100644 --- a/tests/Io/RequestHeaderParserTest.php +++ b/tests/Io/RequestHeaderParserTest.php @@ -4,6 +4,7 @@ use React\Http\Io\RequestHeaderParser; use React\Tests\Http\TestCase; +use Psr\Http\Message\ServerRequestInterface; class RequestHeaderParserTest extends TestCase { @@ -59,16 +60,14 @@ public function testFeedTwoRequestsOnSeparateConnections() $this->assertEquals(2, $called); } - public function testHeadersEventShouldReturnRequestAndBodyBufferAndConnection() + public function testHeadersEventShouldEmitRequestAndConnection() { $request = null; - $bodyBuffer = null; $conn = null; $parser = new RequestHeaderParser(); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer, $connection) use (&$request, &$bodyBuffer, &$conn) { + $parser->on('headers', function ($parsedRequest, $connection) use (&$request, &$conn) { $request = $parsedRequest; - $bodyBuffer = $parsedBodyBuffer; $conn = $connection; }); @@ -76,7 +75,6 @@ public function testHeadersEventShouldReturnRequestAndBodyBufferAndConnection() $parser->handle($connection); $data = $this->createGetRequest(); - $data .= 'RANDOM DATA'; $connection->emit('data', array($data)); $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $request); @@ -85,28 +83,135 @@ public function testHeadersEventShouldReturnRequestAndBodyBufferAndConnection() $this->assertSame('1.1', $request->getProtocolVersion()); $this->assertSame(array('Host' => array('example.com'), 'Connection' => array('close')), $request->getHeaders()); - $this->assertSame('RANDOM DATA', $bodyBuffer); - $this->assertSame($connection, $conn); } - public function testHeadersEventShouldReturnBinaryBodyBuffer() + public function testHeadersEventShouldEmitRequestWhichShouldEmitEndForStreamingBodyWithoutContentLengthFromInitialRequestBody() { - $bodyBuffer = null; + $parser = new RequestHeaderParser(); + + $ended = false; + $that = $this; + $parser->on('headers', function (ServerRequestInterface $request) use (&$ended, $that) { + $body = $request->getBody(); + $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + + $body->on('end', function () use (&$ended) { + $ended = true; + }); + }); + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $data = "GET / HTTP/1.0\r\n\r\n"; + $connection->emit('data', array($data)); + + $this->assertTrue($ended); + } + + public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDataFromInitialRequestBody() + { $parser = new RequestHeaderParser(); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$bodyBuffer) { - $bodyBuffer = $parsedBodyBuffer; + + $buffer = ''; + $that = $this; + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $body = $request->getBody(); + $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + + $body->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + $body->on('end', function () use (&$buffer) { + $buffer .= '.'; + }); }); $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); $parser->handle($connection); - $data = $this->createGetRequest(); - $data .= "\0x01\0x02\0x03\0x04\0x05"; + $data = "POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n"; + $data .= 'RANDOM DATA'; $connection->emit('data', array($data)); - $this->assertSame("\0x01\0x02\0x03\0x04\0x05", $bodyBuffer); + $this->assertSame('RANDOM DATA.', $buffer); + } + + public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyWithPlentyOfDataFromInitialRequestBody() + { + $parser = new RequestHeaderParser(); + + $buffer = ''; + $that = $this; + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $body = $request->getBody(); + $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + + $body->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + }); + + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $size = 10000; + $data = "POST / HTTP/1.0\r\nContent-Length: $size\r\n\r\n"; + $data .= str_repeat('x', $size); + $connection->emit('data', array($data)); + + $this->assertSame($size, strlen($buffer)); + } + + public function testHeadersEventShouldEmitRequestWhichShouldNotEmitStreamingBodyDataWithoutContentLengthFromInitialRequestBody() + { + $parser = new RequestHeaderParser(); + + $buffer = ''; + $that = $this; + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $body = $request->getBody(); + $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + + $body->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + }); + + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $data = "POST / HTTP/1.0\r\n\r\n"; + $data .= 'RANDOM DATA'; + $connection->emit('data', array($data)); + + $this->assertSame('', $buffer); + } + + public function testHeadersEventShouldEmitRequestWhichShouldEmitStreamingBodyDataUntilContentLengthBoundaryFromInitialRequestBody() + { + $parser = new RequestHeaderParser(); + + $buffer = ''; + $that = $this; + $parser->on('headers', function (ServerRequestInterface $request) use (&$buffer, $that) { + $body = $request->getBody(); + $that->assertInstanceOf('React\Stream\ReadableStreamInterface', $body); + + $body->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + }); + + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $data = "POST / HTTP/1.0\r\nContent-Length: 6\r\n\r\n"; + $data .= 'RANDOM DATA'; + $connection->emit('data', array($data)); + + $this->assertSame('RANDOM', $buffer); } public function testHeadersEventShouldParsePathAndQueryString() @@ -114,7 +219,7 @@ public function testHeadersEventShouldParsePathAndQueryString() $request = null; $parser = new RequestHeaderParser(); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request) { + $parser->on('headers', function ($parsedRequest) use (&$request) { $request = $parsedRequest; }); @@ -197,35 +302,6 @@ public function testHeaderOverflowShouldEmitError() $this->assertSame($connection, $passedConnection); } - public function testHeaderOverflowShouldNotEmitErrorWhenDataExceedsMaxHeaderSize() - { - $request = null; - $bodyBuffer = null; - - $parser = new RequestHeaderParser(); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$bodyBuffer) { - $request = $parsedRequest; - $bodyBuffer = $parsedBodyBuffer; - }); - - $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); - $parser->handle($connection); - - $data = $this->createAdvancedPostRequest(); - $body = str_repeat('A', 8193 - strlen($data)); - $data .= $body; - $connection->emit('data', array($data)); - - $headers = array( - 'Host' => array('example.com'), - 'User-Agent' => array('react/alpha'), - 'Connection' => array('close'), - ); - $this->assertSame($headers, $request->getHeaders()); - - $this->assertSame($body, $bodyBuffer); - } - public function testInvalidEmptyRequestHeadersParseException() { $error = null; @@ -423,6 +499,86 @@ public function testInvalidHttpVersion() $this->assertSame('Received request with invalid protocol version', $error->getMessage()); } + public function testInvalidContentLengthRequestHeaderWillEmitError() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET / HTTP/1.1\r\nContent-Length: foo\r\n\r\n")); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame(400, $error->getCode()); + $this->assertSame('The value of `Content-Length` is not valid', $error->getMessage()); + } + + public function testInvalidRequestWithMultipleContentLengthRequestHeadersWillEmitError() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET / HTTP/1.1\r\nContent-Length: 4\r\nContent-Length: 5\r\n\r\n")); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame(400, $error->getCode()); + $this->assertSame('The value of `Content-Length` is not valid', $error->getMessage()); + } + + public function testInvalidTransferEncodingRequestHeaderWillEmitError() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET / HTTP/1.1\r\nTransfer-Encoding: foo\r\n\r\n")); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame(501, $error->getCode()); + $this->assertSame('Only chunked-encoding is allowed for Transfer-Encoding', $error->getMessage()); + } + + public function testInvalidRequestWithBothTransferEncodingAndContentLengthWillEmitError() + { + $error = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(null)->getMock(); + $parser->handle($connection); + + $connection->emit('data', array("GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n")); + + $this->assertInstanceOf('InvalidArgumentException', $error); + $this->assertSame(400, $error->getCode()); + $this->assertSame('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', $error->getMessage()); + } + public function testServerParamsWillBeSetOnHttpsRequest() { $request = null; diff --git a/tests/StreamingServerTest.php b/tests/StreamingServerTest.php index a7b40029..1e7f7330 100644 --- a/tests/StreamingServerTest.php +++ b/tests/StreamingServerTest.php @@ -528,7 +528,12 @@ public function testRequestResumeWillBeForwardedToConnection() $server->listen($this->socket); $this->socket->emit('connection', array($this->connection)); - $data = $this->createGetRequest(); + $data = "GET / HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Content-Length: 5\r\n"; + $data .= "\r\n"; + $this->connection->emit('data', array($data)); } @@ -1561,72 +1566,6 @@ public function testRequestChunkedTransferEncodingCanBeMixedUpperAndLowerCase() $this->connection->emit('data', array($data)); } - public function testRequestWithMalformedHostWillEmitErrorAndSendErrorResponse() - { - $error = null; - $server = new StreamingServer($this->expectCallableNever()); - $server->on('error', function ($message) use (&$error) { - $error = $message; - }); - - $buffer = ''; - - $this->connection - ->expects($this->any()) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); - - $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - - $data = "GET / HTTP/1.1\r\nHost: ///\r\n\r\n"; - $this->connection->emit('data', array($data)); - - $this->assertInstanceOf('InvalidArgumentException', $error); - - $this->assertContains("HTTP/1.1 400 Bad Request\r\n", $buffer); - $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); - } - - public function testRequestWithInvalidHostUriComponentsWillEmitErrorAndSendErrorResponse() - { - $error = null; - $server = new StreamingServer($this->expectCallableNever()); - $server->on('error', function ($message) use (&$error) { - $error = $message; - }); - - $buffer = ''; - - $this->connection - ->expects($this->any()) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); - - $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - - $data = "GET / HTTP/1.1\r\nHost: localhost:80/test\r\n\r\n"; - $this->connection->emit('data', array($data)); - - $this->assertInstanceOf('InvalidArgumentException', $error); - - $this->assertContains("HTTP/1.1 400 Bad Request\r\n", $buffer); - $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); - } - public function testRequestContentLengthWillEmitDataEventAndEndEventAndAdditionalDataWillBeIgnored() { $dataEvent = $this->expectCallableOnceWith('hello'); @@ -1773,155 +1712,6 @@ public function testRequestZeroContentLengthWillEmitEndAndAdditionalDataWillBeIg $this->connection->emit('data', array($data)); } - public function testRequestWithBothContentLengthAndTransferEncodingWillEmitServerErrorAndSendResponse() - { - $error = null; - $server = new StreamingServer($this->expectCallableNever()); - $server->on('error', function ($message) use (&$error) { - $error = $message; - }); - - $buffer = ''; - $this->connection - ->expects($this->any()) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); - - $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - - $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; - $data .= "Connection: close\r\n"; - $data .= "Content-Length: 5\r\n"; - $data .= "Transfer-Encoding: chunked\r\n"; - $data .= "\r\n"; - $data .= "hello"; - - $this->connection->emit('data', array($data)); - - $this->assertContains("HTTP/1.1 400 Bad Request\r\n", $buffer); - $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); - $this->assertInstanceOf('InvalidArgumentException', $error); - } - - public function testRequestInvalidNonIntegerContentLengthWillEmitServerErrorAndSendResponse() - { - $error = null; - $server = new StreamingServer($this->expectCallableNever()); - $server->on('error', function ($message) use (&$error) { - $error = $message; - }); - - $buffer = ''; - $this->connection - ->expects($this->any()) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); - - $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - - $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; - $data .= "Connection: close\r\n"; - $data .= "Content-Length: bla\r\n"; - $data .= "\r\n"; - $data .= "hello"; - - $this->connection->emit('data', array($data)); - - $this->assertContains("HTTP/1.1 400 Bad Request\r\n", $buffer); - $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); - $this->assertInstanceOf('InvalidArgumentException', $error); - } - - public function testRequestInvalidHeadRequestWithInvalidNonIntegerContentLengthWillEmitServerErrorAndSendResponseWithoutBody() - { - $error = null; - $server = new StreamingServer($this->expectCallableNever()); - $server->on('error', function ($message) use (&$error) { - $error = $message; - }); - - $buffer = ''; - $this->connection - ->expects($this->any()) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); - - $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - - $data = "HEAD / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; - $data .= "Connection: close\r\n"; - $data .= "Content-Length: bla\r\n"; - $data .= "\r\n"; - $data .= "hello"; - - $this->connection->emit('data', array($data)); - - $this->assertContains("HTTP/1.1 400 Bad Request\r\n", $buffer); - $this->assertNotContains("\r\n\r\nError 400: Bad Request", $buffer); - $this->assertInstanceOf('InvalidArgumentException', $error); - } - - public function testRequestInvalidMultipleContentLengthWillEmitErrorOnServer() - { - $error = null; - $server = new StreamingServer($this->expectCallableNever()); - $server->on('error', function ($message) use (&$error) { - $error = $message; - }); - - $buffer = ''; - $this->connection - ->expects($this->any()) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); - - $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - - $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; - $data .= "Connection: close\r\n"; - $data .= "Content-Length: 5, 3, 4\r\n"; - $data .= "\r\n"; - $data .= "hello"; - - $this->connection->emit('data', array($data)); - - $this->assertContains("HTTP/1.1 400 Bad Request\r\n", $buffer); - $this->assertContains("\r\n\r\nError 400: Bad Request", $buffer); - $this->assertInstanceOf('InvalidArgumentException', $error); - } - public function testRequestInvalidChunkHeaderTooLongWillEmitErrorOnRequestStream() { $errorEvent = $this->expectCallableOnceWith($this->isInstanceOf('Exception')); @@ -2402,78 +2192,6 @@ function ($data) use (&$buffer) { $this->assertEquals("HTTP/1.1 200 OK\r\nSet-Cookie: name=test\r\nSet-Cookie: session=abc\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", $buffer); } - public function testRequestOnlyChunkedEncodingIsAllowedForTransferEncoding() - { - $error = null; - - $server = new StreamingServer($this->expectCallableNever()); - $server->on('error', function ($exception) use (&$error) { - $error = $exception; - }); - - $buffer = ''; - $this->connection - ->expects($this->any()) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); - - $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - - $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; - $data .= "Connection: close\r\n"; - $data .= "Transfer-Encoding: custom\r\n"; - $data .= "\r\n"; - - $this->connection->emit('data', array($data)); - - $this->assertContains("HTTP/1.1 501 Not Implemented\r\n", $buffer); - $this->assertContains("\r\n\r\nError 501: Not Implemented", $buffer); - $this->assertInstanceOf('InvalidArgumentException', $error); - } - - public function testRequestOnlyChunkedEncodingIsAllowedForTransferEncodingWithHttp10() - { - $error = null; - - $server = new StreamingServer($this->expectCallableNever()); - $server->on('error', function ($exception) use (&$error) { - $error = $exception; - }); - - $buffer = ''; - $this->connection - ->expects($this->any()) - ->method('write') - ->will( - $this->returnCallback( - function ($data) use (&$buffer) { - $buffer .= $data; - } - ) - ); - - $server->listen($this->socket); - $this->socket->emit('connection', array($this->connection)); - - $data = "GET / HTTP/1.0\r\n"; - $data .= "Transfer-Encoding: custom\r\n"; - $data .= "\r\n"; - - $this->connection->emit('data', array($data)); - - $this->assertContains("HTTP/1.0 501 Not Implemented\r\n", $buffer); - $this->assertContains("\r\n\r\nError 501: Not Implemented", $buffer); - $this->assertInstanceOf('InvalidArgumentException', $error); - } - public function testReponseWithExpectContinueRequestContainsContinueWithLaterResponse() { $server = new StreamingServer(function (ServerRequestInterface $request) {